policy-pattern 0.1.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 (36) hide show
  1. policy_pattern/__init__.py +0 -0
  2. policy_pattern/adapters/__init__.py +3 -0
  3. policy_pattern/adapters/memory.py +135 -0
  4. policy_pattern/errors/__init__.py +19 -0
  5. policy_pattern/errors/base_error.py +28 -0
  6. policy_pattern/errors/evaluation_error.py +16 -0
  7. policy_pattern/errors/identifier_error.py +15 -0
  8. policy_pattern/errors/model_validation_error.py +15 -0
  9. policy_pattern/errors/mutation_error.py +15 -0
  10. policy_pattern/errors/relation_error.py +15 -0
  11. policy_pattern/errors/unknown_name_error.py +16 -0
  12. policy_pattern/errors/unknown_type_error.py +16 -0
  13. policy_pattern/evaluator.py +285 -0
  14. policy_pattern/models/__init__.py +34 -0
  15. policy_pattern/models/_schema_validation.py +130 -0
  16. policy_pattern/models/compiled_model.py +43 -0
  17. policy_pattern/models/direct_subject.py +47 -0
  18. policy_pattern/models/identifier.py +55 -0
  19. policy_pattern/models/model.py +46 -0
  20. policy_pattern/models/mutation.py +52 -0
  21. policy_pattern/models/object_reference.py +42 -0
  22. policy_pattern/models/operators.py +116 -0
  23. policy_pattern/models/reference.py +36 -0
  24. policy_pattern/models/relation.py +44 -0
  25. policy_pattern/models/subject.py +22 -0
  26. policy_pattern/models/subject_set.py +56 -0
  27. policy_pattern/models/tuple.py +52 -0
  28. policy_pattern/models/tuple_to_userset.py +51 -0
  29. policy_pattern/models/type.py +71 -0
  30. policy_pattern/ports/__init__.py +7 -0
  31. policy_pattern/ports/tuple_store.py +118 -0
  32. policy_pattern/py.typed +0 -0
  33. policy_pattern-0.1.0.dist-info/METADATA +148 -0
  34. policy_pattern-0.1.0.dist-info/RECORD +36 -0
  35. policy_pattern-0.1.0.dist-info/WHEEL +4 -0
  36. policy_pattern-0.1.0.dist-info/licenses/LICENSE.md +201 -0
File without changes
@@ -0,0 +1,3 @@
1
+ from .memory import MemoryTupleStore
2
+
3
+ __all__ = ["MemoryTupleStore"]
@@ -0,0 +1,135 @@
1
+ """
2
+ In-memory TupleStore adapter — demo/test adapter, no throughput requirement.
3
+ """
4
+
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+ from heapq import nsmallest
8
+ from threading import Lock
9
+ from typing import override
10
+
11
+ from policy_pattern.models import Mutation, ObjectReference, Subject, Tuple
12
+ from policy_pattern.ports import ReadResult, TupleStore, View
13
+
14
+
15
+ class MemoryTupleStore(TupleStore):
16
+ """
17
+ In-memory `TupleStore` — a plain `set[Tuple]` behind a lock, no index.
18
+
19
+ `read_relation()` does a linear scan over a snapshot; no
20
+ `dict[(object, relation), set[Tuple]]` index is maintained. An index
21
+ would need to stay in lockstep with `write_tuples()` on every call —
22
+ real complexity bought for a performance property this adapter is
23
+ explicitly not required to have (`docs/semantics.md` § Threading calls
24
+ it a "demo/test adapter with no throughput requirement"). Real
25
+ throughput is `PostgresTupleStore`'s job, not this one's.
26
+
27
+ Example:
28
+ store = MemoryTupleStore()
29
+ store.write_tuples(Mutation(additions=frozenset({t}), deletions=frozenset()))
30
+ with store.snapshot() as view:
31
+ view.read_tuple(subject, "owner", object)
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ """
36
+ Starts empty.
37
+ """
38
+ self._tuples: set[Tuple] = set()
39
+ self._lock = Lock()
40
+
41
+ @override
42
+ def write_tuples(self, mutation: Mutation) -> None:
43
+ """
44
+ Atomically applies additions/deletions under the lock.
45
+
46
+ `Mutation.__post_init__` already guarantees `additions`/`deletions`
47
+ are disjoint — this method does not re-validate that.
48
+ """
49
+ with self._lock:
50
+ self._tuples = (self._tuples - mutation.deletions) | mutation.additions
51
+
52
+ @override
53
+ @contextmanager
54
+ def snapshot(self) -> Generator[View, None, None]:
55
+ """
56
+ Copies the current tuple set under the lock, then yields a view over the copy.
57
+
58
+ The copy — not a re-held lock — is what makes the view stable: once
59
+ the copy exists, nothing that happens to the live set afterward can
60
+ be observed through the already-open view, because the view never
61
+ looks at the live set again.
62
+ """
63
+ with self._lock:
64
+ frozen = frozenset(self._tuples)
65
+ yield _MemoryView(frozen)
66
+
67
+
68
+ class _MemoryView(View):
69
+ """
70
+ Read-only view over one immutable snapshot of a `MemoryTupleStore`.
71
+
72
+ Private: nothing outside `MemoryTupleStore.snapshot()` ever constructs
73
+ one directly.
74
+ """
75
+
76
+ def __init__(self, tuples: frozenset[Tuple]) -> None:
77
+ """
78
+ Wraps an already-copied, immutable tuple set.
79
+ """
80
+ self._tuples = tuples
81
+
82
+ @property
83
+ def tuples(self) -> frozenset[Tuple]:
84
+ """
85
+ Returns the snapshot's tuples.
86
+
87
+ Private: nothing outside `MemoryTupleStore.snapshot()` ever needs
88
+ to see this.
89
+ """
90
+ return self._tuples
91
+
92
+ @override
93
+ def read_tuple(self, subject: Subject, relation: str, object: ObjectReference) -> bool:
94
+ """
95
+ Checks exact membership by constructing the equivalent Tuple.
96
+
97
+ Reuses `Tuple.__post_init__`'s own validation (delegating `relation`
98
+ to `Identifier`) instead of duplicating it here.
99
+ """
100
+ return Tuple(subject=subject, relation=relation, object=object) in self.tuples
101
+
102
+ @override
103
+ def read_relation(self, object: ObjectReference, relation: str, limit: int) -> ReadResult:
104
+ """
105
+ Linearly scans the snapshot for matches, bounded by `limit + 1`.
106
+
107
+ Args:
108
+ object: The `ObjectReference` to match.
109
+ relation: The relation name to match.
110
+ limit: The maximum number of matches to return. If more than
111
+ `limit` matches exist, the result is truncated to exactly
112
+ `limit` and `complete=False`.
113
+
114
+ Matches are ranked by `repr()` before truncating — `frozenset`
115
+ iteration order is not stable (hash randomization), and an
116
+ unordered scan would make which tuples survive truncation vary
117
+ between runs with identical data, breaking
118
+ docs/architecture.md's determinism guarantee. `heapq.nsmallest`
119
+ keeps only `limit + 1` candidates in memory at a time, rather than
120
+ `sorted()`'s full match set — with a huge match set and a small
121
+ limit, that is the difference between O(limit) and O(match count)
122
+ memory. `limit=0` needs no special-casing: it is already
123
+ well-defined (immediately incomplete if any match exists,
124
+ complete-and-empty otherwise).
125
+ """
126
+ candidates: list[Tuple] = nsmallest(
127
+ limit + 1,
128
+ (t for t in self.tuples if t.object == object and t.relation == relation),
129
+ key=repr,
130
+ )
131
+
132
+ if len(candidates) > limit:
133
+ return ReadResult(tuples=frozenset(candidates[:limit]), complete=False)
134
+
135
+ return ReadResult(tuples=frozenset(candidates), complete=True)
@@ -0,0 +1,19 @@
1
+ from .base_error import BaseError
2
+ from .evaluation_error import EvaluationError
3
+ from .identifier_error import InvalidIdentifierError
4
+ from .model_validation_error import ModelValidationError
5
+ from .mutation_error import InvalidMutationError
6
+ from .relation_error import InvalidRelationError
7
+ from .unknown_name_error import UnknownNameError
8
+ from .unknown_type_error import UnknownTypeError
9
+
10
+ __all__ = [
11
+ "BaseError",
12
+ "EvaluationError",
13
+ "InvalidIdentifierError",
14
+ "InvalidMutationError",
15
+ "InvalidRelationError",
16
+ "ModelValidationError",
17
+ "UnknownNameError",
18
+ "UnknownTypeError",
19
+ ]
@@ -0,0 +1,28 @@
1
+ """
2
+ Base class for all policy pattern errors.
3
+ To create a standardized error response, inherit from this class and set the `code` attribute to a unique error code.
4
+ """
5
+
6
+ from typing import ClassVar
7
+
8
+
9
+ class BaseError(Exception):
10
+ """
11
+ Custom exception class for policy pattern errors.
12
+ It provides a standardized error response format with a unique error code, message,
13
+ and optional related information.
14
+ """
15
+
16
+ code: ClassVar[str]
17
+
18
+ def __init__(self, *, message: str, related: str | None = None) -> None:
19
+ """
20
+ Initialize the BaseError following the standard error response format in the policy pattern docs errors.md.
21
+
22
+ Args:
23
+ message (str): A description of the error.
24
+ related (str | None): Additional information about the error, such as the field or type that caused it.
25
+ """
26
+ self.message = message
27
+ self.related = related
28
+ super().__init__(f"[{self.code}] {message}")
@@ -0,0 +1,16 @@
1
+ """
2
+ Set of errors related to Evaluator runtime failures (budget exceeded, cycle detected).
3
+ """
4
+
5
+ from .base_error import BaseError
6
+
7
+
8
+ class EvaluationError(BaseError):
9
+ """
10
+ Error raised when check() cannot complete: a cycle was detected, or a configured
11
+ EvaluationBudget limit (max_depth, max_nodes, max_store_reads) was exceeded.
12
+ Attributes:
13
+ code (ClassVar[str]): A unique error code for the specific error type.
14
+ """
15
+
16
+ code = "PP201"
@@ -0,0 +1,15 @@
1
+ """
2
+ Set of errors related to Identifier class.
3
+ """
4
+
5
+ from .base_error import BaseError
6
+
7
+
8
+ class InvalidIdentifierError(BaseError):
9
+ """
10
+ Error raised when an invalid identifier is encountered.
11
+ Attributes:
12
+ code (ClassVar[str]): A unique error code for the specific error type.
13
+ """
14
+
15
+ code = "PP001"
@@ -0,0 +1,15 @@
1
+ """
2
+ Set of errors related to Model validation at compile() time.
3
+ """
4
+
5
+ from .base_error import BaseError
6
+
7
+
8
+ class ModelValidationError(BaseError):
9
+ """
10
+ Error raised when a Model fails compile()-time validation.
11
+ Attributes:
12
+ code (ClassVar[str]): A unique error code for the specific error type.
13
+ """
14
+
15
+ code = "PP101"
@@ -0,0 +1,15 @@
1
+ """
2
+ Set of errors related to Mutation class.
3
+ """
4
+
5
+ from .base_error import BaseError
6
+
7
+
8
+ class InvalidMutationError(BaseError):
9
+ """
10
+ Error raised when an invalid mutation is encountered.
11
+ Attributes:
12
+ code (ClassVar[str]): A unique error code for the specific error type.
13
+ """
14
+
15
+ code = "PP003"
@@ -0,0 +1,15 @@
1
+ """
2
+ Set of errors related to Relation class.
3
+ """
4
+
5
+ from .base_error import BaseError
6
+
7
+
8
+ class InvalidRelationError(BaseError):
9
+ """
10
+ Error raised when an invalid relation is encountered.
11
+ Attributes:
12
+ code (ClassVar[str]): A unique error code for the specific error type.
13
+ """
14
+
15
+ code = "PP002"
@@ -0,0 +1,16 @@
1
+ """
2
+ Set of errors related to referencing a relation/permission name not declared on a type.
3
+ """
4
+
5
+ from .base_error import BaseError
6
+
7
+
8
+ class UnknownNameError(BaseError):
9
+ """
10
+ Error raised when check() references a name that is declared as neither a relation
11
+ nor a permission on the object's (already-known) type.
12
+ Attributes:
13
+ code (ClassVar[str]): A unique error code for the specific error type.
14
+ """
15
+
16
+ code = "PP203"
@@ -0,0 +1,16 @@
1
+ """
2
+ Set of errors related to referencing a type the CompiledModel does not declare.
3
+ """
4
+
5
+ from .base_error import BaseError
6
+
7
+
8
+ class UnknownTypeError(BaseError):
9
+ """
10
+ Error raised when check() (or internal userset-expansion recursion) references an
11
+ ObjectReference whose type is not declared in the CompiledModel.
12
+ Attributes:
13
+ code (ClassVar[str]): A unique error code for the specific error type.
14
+ """
15
+
16
+ code = "PP202"
@@ -0,0 +1,285 @@
1
+ """
2
+ Evaluator: resolves check() over a CompiledModel and a TupleStore.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from .errors import EvaluationError, UnknownNameError, UnknownTypeError
8
+ from .models import (
9
+ CompiledModel,
10
+ DirectSubject,
11
+ Exclusion,
12
+ Intersection,
13
+ ObjectReference,
14
+ PermissionExpression,
15
+ Reference,
16
+ Subject,
17
+ SubjectSet,
18
+ TupleToUserset,
19
+ Union,
20
+ )
21
+ from .ports import TupleStore, View
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class EvaluationBudget:
26
+ """
27
+ Per-`check()` resource limits.
28
+
29
+ No `__post_init__` validation — a degenerate value (e.g. `max_depth=0`)
30
+ is well-defined without a guard: it simply fails on the first node
31
+ `Evaluator` visits. Same reasoning already applied to `read_relation`'s
32
+ `limit=0` — no invariant to enforce, trust the caller.
33
+
34
+ Example:
35
+ EvaluationBudget(max_depth=10, max_nodes=500, max_store_reads=100)
36
+ """
37
+
38
+ # Maximum recursion depth for a single `check()` call. Each recursive call
39
+ # to `_check_name()` adds one to the depth; exceeding this limit raises an
40
+ # `EvaluationError`.
41
+ max_depth: int = 25
42
+
43
+ # Maximum number of nodes visited during a single `check()` call. Each
44
+ # call to `_check_name()` increments this count; exceeding this limit raises an `EvaluationError`.
45
+ max_nodes: int = 10_000
46
+
47
+ # Maximum number of store reads during a single `check()` call. Each call
48
+ # to `_charge_read()` increments this count; exceeding this limit raises an `EvaluationError`.
49
+ max_store_reads: int = 1_000
50
+
51
+
52
+ @dataclass(slots=True)
53
+ class EvaluationContext:
54
+ """
55
+ Live bookkeeping for exactly one `check()` call.
56
+
57
+ Deliberately mutable, unlike almost everything else in this package —
58
+ this is not a domain value, it is the in-flight state of one
59
+ evaluation, never seen or reused outside that call's lifetime.
60
+
61
+ `len(visited)` doubles as the current recursion depth: `visited` only
62
+ ever holds ancestors currently in progress on the live call path
63
+ (added before recursing, discarded after returning) — anything
64
+ permanently resolved lives in `memo` instead, so the two never overlap
65
+ in meaning.
66
+ """
67
+
68
+ view: View
69
+ budget: EvaluationBudget
70
+ visited: set[tuple[ObjectReference, str]] = field(default_factory=lambda: set[tuple[ObjectReference, str]]())
71
+ memo: dict[tuple[ObjectReference, str], bool] = field(
72
+ default_factory=lambda: dict[tuple[ObjectReference, str], bool]()
73
+ )
74
+ node_count: int = 0
75
+ store_read_count: int = 0
76
+
77
+
78
+ class Evaluator:
79
+ """
80
+ Resolves check(subject, name, object) against a CompiledModel and a TupleStore.
81
+
82
+ Example:
83
+ evaluator = Evaluator(model=compiled_model, store=memory_store)
84
+ evaluator.check(subject, "viewer", document)
85
+ """
86
+
87
+ def __init__(self, model: CompiledModel, store: TupleStore) -> None:
88
+ """
89
+ Binds this evaluator to one CompiledModel and one TupleStore.
90
+ """
91
+ self._model = model
92
+ self._store = store
93
+
94
+ def check(
95
+ self,
96
+ subject: Subject,
97
+ name: str,
98
+ object: ObjectReference,
99
+ budget: EvaluationBudget | None = None,
100
+ ) -> bool:
101
+ """
102
+ Returns whether subject holds relation/permission name on object.
103
+
104
+ Opens exactly one store.snapshot() for the lifetime of this call —
105
+ docs/semantics.md's snapshot-consistency requirement — every read
106
+ this call triggers, however deep the recursion, goes through the
107
+ same View.
108
+
109
+ `budget` defaults to `None`/`EvaluationBudget()` rather than a
110
+ bare `EvaluationBudget()` default argument — Python evaluates a
111
+ default argument once, at function-definition time, so every
112
+ omitted-budget call would share one instance. Harmless here since
113
+ `EvaluationBudget` is frozen, but `ruff`'s `B008` flags the pattern
114
+ regardless of mutability, and the codebase doesn't carve out an
115
+ exception for "this particular immutable case."
116
+ """
117
+ if budget is None:
118
+ budget = EvaluationBudget()
119
+ with self._store.snapshot() as view:
120
+ context = EvaluationContext(view=view, budget=budget)
121
+ return self._check_name(subject, name, object, context)
122
+
123
+ def _check_name(self, subject: Subject, name: str, object: ObjectReference, context: EvaluationContext) -> bool:
124
+ """
125
+ Dispatches (object, name) to a relation or permission check, with cycle/memo/budget bookkeeping.
126
+ """
127
+ key = (object, name)
128
+ if key in context.memo:
129
+ return context.memo[key]
130
+ if key in context.visited:
131
+ raise EvaluationError(
132
+ message=f"cycle detected: {name!r} on {object!r} is already being evaluated for {subject!r}",
133
+ related=f"object={object!r}",
134
+ )
135
+
136
+ if len(context.visited) >= context.budget.max_depth:
137
+ raise EvaluationError(
138
+ message=f"max_depth={context.budget.max_depth!r} exceeded evaluating {name!r} on {object!r}",
139
+ related=f"object={object!r}",
140
+ )
141
+ context.node_count += 1
142
+ if context.node_count > context.budget.max_nodes:
143
+ raise EvaluationError(
144
+ message=f"max_nodes={context.budget.max_nodes!r} exceeded evaluating {name!r} on {object!r}",
145
+ related=f"object={object!r}",
146
+ )
147
+
148
+ context.visited.add(key)
149
+ try:
150
+ try:
151
+ type_declaration = self._model.types[object.type]
152
+ except KeyError:
153
+ raise UnknownTypeError(
154
+ message=f"type {object.type!r} is not declared in this CompiledModel",
155
+ related=f"object={object!r}",
156
+ ) from None
157
+ if name in type_declaration.relations:
158
+ result = self._check_relation(subject, name, object, context)
159
+ elif name in type_declaration.permissions:
160
+ result = self._check_permission(subject, type_declaration.permissions[name], object, context)
161
+ else:
162
+ raise UnknownNameError(
163
+ message=f"{name!r} is not declared as a relation or permission on {object.type!r}",
164
+ related=f"object={object!r}",
165
+ )
166
+ finally:
167
+ context.visited.discard(key)
168
+
169
+ context.memo[key] = result
170
+ return result
171
+
172
+ def _check_relation(self, subject: Subject, name: str, object: ObjectReference, context: EvaluationContext) -> bool:
173
+ """
174
+ Exact tuple lookup, then bounded userset expansion for any SubjectSet found.
175
+
176
+ Every candidate subject's shape is checked against this Relation's
177
+ own `allowed_subject_types` before it counts — `TupleStore` is
178
+ schema-agnostic by design (docs/architecture.md), so it never
179
+ rejects a malformed write; this is the one place that knows both
180
+ the schema and the stored fact, so it is the actual trust boundary.
181
+ A tuple whose subject shape isn't declared is inert: never a match,
182
+ never followed.
183
+ """
184
+ relation = self._model.types[object.type].relations[name]
185
+
186
+ def shape_allowed(candidate: Subject) -> bool:
187
+ if isinstance(candidate, DirectSubject):
188
+ return candidate.object.type in relation.allowed_subject_types
189
+ return f"{candidate.object.type}#{candidate.relation}" in relation.allowed_subject_types
190
+
191
+ self._charge_read(context, object, name)
192
+ if shape_allowed(subject) and context.view.read_tuple(subject, name, object):
193
+ return True
194
+
195
+ self._charge_read(context, object, name)
196
+ result = context.view.read_relation(object, name, limit=context.budget.max_nodes - context.node_count)
197
+ if not result.complete:
198
+ raise EvaluationError(
199
+ message=f"enumeration of {name!r} on {object!r} truncated by remaining max_nodes budget",
200
+ related=f"object={object!r}",
201
+ )
202
+
203
+ return any(
204
+ isinstance(t.subject, SubjectSet)
205
+ and shape_allowed(t.subject)
206
+ and self._check_name(subject, t.subject.relation, t.subject.object, context)
207
+ for t in sorted(result.tuples, key=repr)
208
+ )
209
+
210
+ def _check_permission(
211
+ self, subject: Subject, expression: PermissionExpression, object: ObjectReference, context: EvaluationContext
212
+ ) -> bool:
213
+ """
214
+ Recursively evaluates a PermissionExpression tree.
215
+
216
+ Union/Intersection may use Python's or/and directly: if the left
217
+ operand already decides the boolean result, skipping the right one
218
+ cannot produce an incorrect decision (docs/semantics.md forbids
219
+ short-circuiting only for Exclusion, where skipping `exclude` could
220
+ hide a real subtraction). Python's or/and evaluate left-to-right by
221
+ language specification, satisfying the determinism requirement for
222
+ free.
223
+ """
224
+ if isinstance(expression, Reference):
225
+ return self._check_name(subject, expression.name, object, context)
226
+ if isinstance(expression, TupleToUserset):
227
+ return self._check_tuple_to_userset(subject, expression, object, context)
228
+ if isinstance(expression, (Union, Intersection)):
229
+ left = self._check_permission(subject, expression.left, object, context)
230
+ if isinstance(expression, Union):
231
+ return left or self._check_permission(subject, expression.right, object, context)
232
+ return left and self._check_permission(subject, expression.right, object, context)
233
+ if isinstance(expression, Exclusion):
234
+ include = self._check_permission(subject, expression.include, object, context)
235
+ exclude = self._check_permission(subject, expression.exclude, object, context)
236
+ return include and not exclude
237
+ raise EvaluationError(
238
+ message=f"unknown expression type {type(expression).__name__!r}",
239
+ related=f"object={object!r}",
240
+ )
241
+
242
+ def _check_tuple_to_userset(
243
+ self, subject: Subject, expression: TupleToUserset, object: ObjectReference, context: EvaluationContext
244
+ ) -> bool:
245
+ """
246
+ Follows expression.tupleset to each related object, then checks expression.computed there.
247
+
248
+ Only DirectSubject entries are followed — a SubjectSet stored under
249
+ the tupleset relation has no single "related object" to jump to, so
250
+ it does not participate in traversal. A DirectSubject whose type
251
+ isn't declared in the tupleset Relation's own `allowed_subject_types`
252
+ is malformed data and is skipped the same way `_check_relation`
253
+ skips a malformed direct/userset match.
254
+ """
255
+ relation = self._model.types[object.type].relations[expression.tupleset]
256
+
257
+ self._charge_read(context, object, expression.tupleset)
258
+ result = context.view.read_relation(
259
+ object, expression.tupleset, limit=context.budget.max_nodes - context.node_count
260
+ )
261
+ if not result.complete:
262
+ raise EvaluationError(
263
+ message=f"enumeration of {expression.tupleset!r} on {object!r} truncated by remaining max_nodes budget",
264
+ related=f"object={object!r}",
265
+ )
266
+
267
+ return any(
268
+ isinstance(t.subject, DirectSubject)
269
+ and t.subject.object.type in relation.allowed_subject_types
270
+ and self._check_name(subject, expression.computed, t.subject.object, context)
271
+ for t in sorted(result.tuples, key=repr)
272
+ )
273
+
274
+ def _charge_read(self, context: EvaluationContext, object: ObjectReference, name: str) -> None:
275
+ """
276
+ Counts one store read, raising EvaluationError if max_store_reads is now exceeded.
277
+ """
278
+ context.store_read_count += 1
279
+ if context.store_read_count > context.budget.max_store_reads:
280
+ raise EvaluationError(
281
+ message=(
282
+ f"max_store_reads={context.budget.max_store_reads!r} exceeded evaluating {name!r} on {object!r}"
283
+ ),
284
+ related=f"object={object!r}",
285
+ )
@@ -0,0 +1,34 @@
1
+ from .compiled_model import CompiledModel
2
+ from .direct_subject import DirectSubject
3
+ from .identifier import Identifier
4
+ from .model import Model
5
+ from .mutation import Mutation
6
+ from .object_reference import ObjectReference
7
+ from .operators import Exclusion, Intersection, PermissionExpression, Union
8
+ from .reference import Reference
9
+ from .relation import Relation
10
+ from .subject import Subject
11
+ from .subject_set import SubjectSet
12
+ from .tuple import Tuple
13
+ from .tuple_to_userset import TupleToUserset
14
+ from .type import Type
15
+
16
+ __all__ = [
17
+ "CompiledModel",
18
+ "DirectSubject",
19
+ "Exclusion",
20
+ "Identifier",
21
+ "Intersection",
22
+ "Model",
23
+ "Mutation",
24
+ "ObjectReference",
25
+ "PermissionExpression",
26
+ "Reference",
27
+ "Relation",
28
+ "Subject",
29
+ "SubjectSet",
30
+ "Tuple",
31
+ "TupleToUserset",
32
+ "Type",
33
+ "Union",
34
+ ]