policy-pattern 0.4.0__tar.gz → 0.5.0__tar.gz

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 (43) hide show
  1. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/PKG-INFO +8 -11
  2. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/README.md +7 -10
  3. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/adapters/memory.py +25 -0
  4. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/adapters/postgres.py +51 -10
  5. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/evaluator.py +152 -0
  6. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/ports/tuple_store.py +21 -0
  7. policy_pattern-0.5.0/policy_pattern/sql/postgres/0002_add_subject_index.sql +5 -0
  8. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/pyproject.toml +2 -2
  9. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/pyproject.toml.orig +2 -2
  10. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/LICENSE.md +0 -0
  11. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/__init__.py +0 -0
  12. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/adapters/__init__.py +0 -0
  13. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/adapters/_db_protocols.py +0 -0
  14. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/adapters/_tuple_mapping.py +0 -0
  15. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/__init__.py +0 -0
  16. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/base_error.py +0 -0
  17. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/evaluation_error.py +0 -0
  18. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/identifier_error.py +0 -0
  19. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/index_entry_too_large_error.py +0 -0
  20. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/model_validation_error.py +0 -0
  21. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/mutation_error.py +0 -0
  22. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/relation_error.py +0 -0
  23. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/unknown_name_error.py +0 -0
  24. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/errors/unknown_type_error.py +0 -0
  25. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/__init__.py +0 -0
  26. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/_schema_validation.py +0 -0
  27. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/compiled_model.py +0 -0
  28. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/direct_subject.py +0 -0
  29. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/identifier.py +0 -0
  30. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/model.py +0 -0
  31. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/mutation.py +0 -0
  32. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/object_reference.py +0 -0
  33. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/operators.py +0 -0
  34. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/reference.py +0 -0
  35. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/relation.py +0 -0
  36. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/subject.py +0 -0
  37. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/subject_set.py +0 -0
  38. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/tuple.py +0 -0
  39. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/tuple_to_userset.py +0 -0
  40. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/models/type.py +0 -0
  41. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/ports/__init__.py +0 -0
  42. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/py.typed +0 -0
  43. {policy_pattern-0.4.0 → policy_pattern-0.5.0}/policy_pattern/sql/postgres/0001_create_policy_tuples.sql +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: policy-pattern
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: A minimal, embeddable ReBAC authorization engine for Python, inspired by Google Zanzibar.
5
5
  Keywords: python,authorization,access-control,rebac,zanzibar,security
6
6
  Author: Guillem Pozo Sebastián
@@ -45,8 +45,7 @@ Description-Content-Type: text/markdown
45
45
  </a>
46
46
  </p>
47
47
 
48
- `policy-pattern` is a standalone Python library for relationship-based access control (ReBAC). It models
49
- authorization as relationships between subjects and objects and evaluates those relationships through composable
48
+ `policy-pattern` is a standalone Python library for relationship-based access control (ReBAC). It models authorization as relationships between subjects and objects and evaluates those relationships through composable
50
49
  authorization rules.
51
50
 
52
51
  - **No authorization server**
@@ -131,18 +130,15 @@ evaluator = Evaluator(model=compiled, store=store)
131
130
  evaluator.check(alice, "viewer", document_42) # True — alice owns the parent folder
132
131
  ```
133
132
 
134
- `viewer` here means "the document's own owner, or the owner of whatever folder it lives in" — a document under
135
- `folder:engineering` inherits access from that folder without copying a permission onto every document.
133
+ `viewer` here means "the document's own owner, or the owner of whatever folder it lives in" — a document under `folder:engineering` inherits access from that folder without copying a permission onto every document.
136
134
 
137
- See the [Usage Guide](docs/usage/README.md) for the full, runnable walkthrough — every rewrite operator, nested
138
- groups, `EvaluationBudget`, and error handling.
135
+ See the [Usage Guide](docs/usage/README.md) for the full, runnable walkthrough — every rewrite operator, nested groups, `EvaluationBudget`, and error handling.
139
136
 
140
137
  ---
141
138
 
142
139
  ## Documentation
143
140
 
144
- The repository uses specifications and architecture documents as part of the implementation contract — they
145
- describe **what must be true**, independent of any one adapter's implementation.
141
+ The repository uses specifications and architecture documents as part of the implementation contract — they describe **what must be true**, independent of any one adapter's implementation.
146
142
 
147
143
  ```text
148
144
  docs/
@@ -153,8 +149,9 @@ docs/
153
149
  └── errors.md — the PPxxx error code ranges and what each one means
154
150
  ```
155
151
 
156
- Start with the [Usage Guide](docs/usage/README.md) to see the library working end to end, then
157
- [Architecture](docs/architecture.md) to understand why the model is shaped the way it is.
152
+ Start with the [Usage Guide](docs/usage/README.md) to see the library working end to end, then [Architecture](docs/architecture.md) to understand why the model is shaped the way it is.
153
+
154
+ See [`CODE_STYLE.md`](.github/CODE_STYLE.md) and [`TESTING.md`](TESTING.md) for contributor conventions no tool enforces.
158
155
 
159
156
  ---
160
157
 
@@ -20,8 +20,7 @@
20
20
  </a>
21
21
  </p>
22
22
 
23
- `policy-pattern` is a standalone Python library for relationship-based access control (ReBAC). It models
24
- authorization as relationships between subjects and objects and evaluates those relationships through composable
23
+ `policy-pattern` is a standalone Python library for relationship-based access control (ReBAC). It models authorization as relationships between subjects and objects and evaluates those relationships through composable
25
24
  authorization rules.
26
25
 
27
26
  - **No authorization server**
@@ -106,18 +105,15 @@ evaluator = Evaluator(model=compiled, store=store)
106
105
  evaluator.check(alice, "viewer", document_42) # True — alice owns the parent folder
107
106
  ```
108
107
 
109
- `viewer` here means "the document's own owner, or the owner of whatever folder it lives in" — a document under
110
- `folder:engineering` inherits access from that folder without copying a permission onto every document.
108
+ `viewer` here means "the document's own owner, or the owner of whatever folder it lives in" — a document under `folder:engineering` inherits access from that folder without copying a permission onto every document.
111
109
 
112
- See the [Usage Guide](docs/usage/README.md) for the full, runnable walkthrough — every rewrite operator, nested
113
- groups, `EvaluationBudget`, and error handling.
110
+ See the [Usage Guide](docs/usage/README.md) for the full, runnable walkthrough — every rewrite operator, nested groups, `EvaluationBudget`, and error handling.
114
111
 
115
112
  ---
116
113
 
117
114
  ## Documentation
118
115
 
119
- The repository uses specifications and architecture documents as part of the implementation contract — they
120
- describe **what must be true**, independent of any one adapter's implementation.
116
+ The repository uses specifications and architecture documents as part of the implementation contract — they describe **what must be true**, independent of any one adapter's implementation.
121
117
 
122
118
  ```text
123
119
  docs/
@@ -128,8 +124,9 @@ docs/
128
124
  └── errors.md — the PPxxx error code ranges and what each one means
129
125
  ```
130
126
 
131
- Start with the [Usage Guide](docs/usage/README.md) to see the library working end to end, then
132
- [Architecture](docs/architecture.md) to understand why the model is shaped the way it is.
127
+ Start with the [Usage Guide](docs/usage/README.md) to see the library working end to end, then [Architecture](docs/architecture.md) to understand why the model is shaped the way it is.
128
+
129
+ See [`CODE_STYLE.md`](.github/CODE_STYLE.md) and [`TESTING.md`](TESTING.md) for contributor conventions no tool enforces.
133
130
 
134
131
  ---
135
132
 
@@ -133,3 +133,28 @@ class _MemoryView(View):
133
133
  return ReadResult(tuples=frozenset(candidates[:limit]), complete=False)
134
134
 
135
135
  return ReadResult(tuples=frozenset(candidates), complete=True)
136
+
137
+ @override
138
+ def read_tuples_for_subject(self, subject: Subject, limit: int) -> ReadResult:
139
+ """Linearly scans the snapshot for tuples with this exact subject.
140
+
141
+ Args:
142
+ subject (Subject): The exact subject to match.
143
+ limit (int): The maximum number of matching tuples to return.
144
+
145
+ Returns:
146
+ ReadResult: Matching tuples and whether the enumeration is complete.
147
+
148
+ Raises:
149
+ None: This method defines no exception of its own.
150
+ """
151
+ candidates: list[Tuple] = nsmallest(
152
+ limit + 1,
153
+ (t for t in self.tuples if t.subject == subject),
154
+ key=repr,
155
+ )
156
+
157
+ if len(candidates) > limit:
158
+ return ReadResult(tuples=frozenset(candidates[:limit]), complete=False)
159
+
160
+ return ReadResult(tuples=frozenset(candidates), complete=True)
@@ -8,13 +8,16 @@ from importlib.resources import files
8
8
  from typing import Any, override
9
9
 
10
10
  from policy_pattern.errors import IndexEntryTooLargeError
11
- from policy_pattern.models import Mutation, ObjectReference, Subject
11
+ from policy_pattern.models import Mutation, ObjectReference, Subject, Tuple
12
12
  from policy_pattern.ports import ReadResult, TupleStore, View
13
13
 
14
14
  from ._db_protocols import Connection, ConnectionPool, Cursor
15
15
  from ._tuple_mapping import columns, compose, decompose
16
16
 
17
- MIGRATION_RESOURCE: tuple[str, str, str] = ("sql", "postgres", "0001_create_policy_tuples.sql")
17
+ MIGRATION_RESOURCE: tuple[tuple[str, str, str], ...] = (
18
+ ("sql", "postgres", "0001_create_policy_tuples.sql"),
19
+ ("sql", "postgres", "0002_add_subject_index.sql"),
20
+ )
18
21
 
19
22
  # PostgreSQL's B-tree index entry limit for the default 8KB page size
20
23
  # (BLCKSZ). Identifier bounds a value to 255 code points, any printable
@@ -68,6 +71,12 @@ _READ_RELATION_SQL: str = """
68
71
  ORDER BY subject_type, subject_id, subject_relation LIMIT %s
69
72
  """
70
73
 
74
+ _READ_TUPLES_FOR_SUBJECT_SQL: str = """
75
+ SELECT object_type, object_id, relation FROM "authz_tuples"
76
+ WHERE subject_type=%s AND subject_id=%s AND subject_relation=%s
77
+ ORDER BY object_type, object_id, relation LIMIT %s
78
+ """
79
+
71
80
 
72
81
  class PostgresTupleStore(TupleStore):
73
82
  """
@@ -114,16 +123,17 @@ class PostgresTupleStore(TupleStore):
114
123
  failed execute() would otherwise return an aborted transaction to
115
124
  the pool for the next caller to trip over.
116
125
  """
117
- migration_sql = files("policy_pattern").joinpath(*MIGRATION_RESOURCE).read_text(encoding="utf-8")
118
126
  with self._pool.connection() as conn:
119
127
  cursor = conn.cursor()
120
- try:
121
- cursor.execute(migration_sql)
122
- except Exception:
123
- conn.rollback()
124
- raise
125
- else:
126
- conn.commit()
128
+ for resource in MIGRATION_RESOURCE:
129
+ migration_sql = files("policy_pattern").joinpath(*resource).read_text(encoding="utf-8")
130
+ try:
131
+ cursor.execute(migration_sql)
132
+ except Exception:
133
+ conn.rollback()
134
+ raise
135
+ else:
136
+ conn.commit()
127
137
 
128
138
  @override
129
139
  def write_tuples(self, mutation: Mutation) -> None:
@@ -255,3 +265,34 @@ class _PostgresView(View):
255
265
  tuples = frozenset(compose(r, relation, object) for r in rows[:limit])
256
266
 
257
267
  return ReadResult(tuples=tuples, complete=len(rows) <= limit)
268
+
269
+ @override
270
+ def read_tuples_for_subject(self, subject: Subject, limit: int) -> ReadResult:
271
+ """Returns up to `limit` tuples whose subject exactly matches, in one query.
272
+
273
+ Args:
274
+ subject (Subject): The exact subject to match.
275
+ limit (int): The maximum number of matching tuples to return.
276
+
277
+ Returns:
278
+ ReadResult: Matching tuples and whether the enumeration is complete.
279
+
280
+ Raises:
281
+ None: This method defines no exception of its own.
282
+ """
283
+ subject_type, subject_id, subject_relation = decompose(subject)
284
+ cursor: Cursor = self.connection.cursor()
285
+ cursor.execute(
286
+ _READ_TUPLES_FOR_SUBJECT_SQL,
287
+ (subject_type, subject_id, subject_relation, limit + 1),
288
+ )
289
+ rows: list[tuple[Any, ...]] = cursor.fetchall()
290
+ tuples = frozenset(
291
+ Tuple(
292
+ subject=subject,
293
+ relation=row[2],
294
+ object=ObjectReference(type=row[0], id=row[1]),
295
+ )
296
+ for row in rows[:limit]
297
+ )
298
+ return ReadResult(tuples=tuples, complete=len(rows) <= limit)
@@ -2,8 +2,10 @@
2
2
  Evaluator: resolves check() over a CompiledModel and a TupleStore.
3
3
  """
4
4
 
5
+ from collections import deque
5
6
  from collections.abc import Sequence
6
7
  from dataclasses import dataclass, field
8
+ from functools import cached_property
7
9
 
8
10
  from .errors import EvaluationError, UnknownNameError, UnknownTypeError
9
11
  from .models import (
@@ -148,6 +150,156 @@ class Evaluator:
148
150
  context = EvaluationContext(view=view, budget=budget)
149
151
  return [self._check_name(subject, name, object, context) for subject, name, object in requests]
150
152
 
153
+ def list_objects(
154
+ self,
155
+ subject: Subject,
156
+ name: str,
157
+ object_type: str,
158
+ candidate_ids: frozenset[ObjectReference] | None = None,
159
+ budget: EvaluationBudget | None = None,
160
+ ) -> frozenset[ObjectReference]:
161
+ """Returns objects of `object_type` on which `subject` holds `name`.
162
+
163
+ Args:
164
+ subject (Subject): The subject whose access is queried.
165
+ name (str): The relation or permission name to check.
166
+ object_type (str): The type of objects to return.
167
+ candidate_ids (frozenset[ObjectReference] | None): Optional objects to check instead of discovering them.
168
+ budget (EvaluationBudget | None): Resource limits for this evaluation.
169
+
170
+ Returns:
171
+ frozenset[ObjectReference]: Objects of `object_type` allowed by the forward check.
172
+
173
+ Raises:
174
+ UnknownTypeError: If `object_type` is not declared in the CompiledModel.
175
+ UnknownNameError: If `name` is not declared on `object_type`.
176
+ EvaluationError: If reverse candidate discovery or a candidate check exhausts the evaluation budget.
177
+ """
178
+ if object_type not in self._model.types:
179
+ raise UnknownTypeError(
180
+ message=f"type {object_type!r} is not declared in this CompiledModel",
181
+ related=f"object_type={object_type!r}",
182
+ )
183
+ declaration = self._model.types[object_type]
184
+ if name not in declaration.relations and name not in declaration.permissions:
185
+ raise UnknownNameError(
186
+ message=f"{name!r} is not declared as a relation or permission on {object_type!r}",
187
+ related=f"object_type={object_type!r}",
188
+ )
189
+
190
+ if budget is None:
191
+ budget = EvaluationBudget()
192
+
193
+ with self._store.snapshot() as view:
194
+ context = EvaluationContext(view=view, budget=budget)
195
+ candidates = self._reverse_candidates(subject, context) if candidate_ids is None else candidate_ids
196
+
197
+ return frozenset(
198
+ object
199
+ for object in sorted(candidates, key=repr)
200
+ if object.type == object_type and self._check_name(subject, name, object, context)
201
+ )
202
+
203
+ def _reverse_candidates(self, subject: Subject, context: EvaluationContext) -> frozenset[ObjectReference]:
204
+ """Walk tuple subject edges to generate possible objects without interpreting permissions.
205
+
206
+ Args:
207
+ subject (Subject): The starting subject for the reverse walk.
208
+ context (EvaluationContext): The shared view and resource-budget state.
209
+
210
+ Returns:
211
+ frozenset[ObjectReference]: All candidate objects reached by the tuple walk.
212
+
213
+ Raises:
214
+ EvaluationError: If `max_depth` or `max_nodes` is exceeded, or a subject-keyed enumeration is truncated.
215
+ """
216
+ queue: deque[tuple[Subject, int]] = deque([(subject, 0)])
217
+ visited: set[Subject] = {subject}
218
+ candidates: set[ObjectReference] = set()
219
+
220
+ while queue:
221
+ current, depth = queue.popleft()
222
+ context.node_count += 1
223
+ if context.node_count > context.budget.max_nodes:
224
+ raise EvaluationError(
225
+ message=f"max_nodes={context.budget.max_nodes!r} exceeded during reverse lookup",
226
+ related=f"subject={current!r}",
227
+ )
228
+
229
+ self._charge_read(context, current.object, "list_objects")
230
+ result = context.view.read_tuples_for_subject(
231
+ current,
232
+ limit=context.budget.max_nodes - context.node_count,
233
+ )
234
+
235
+ if not result.complete:
236
+ raise EvaluationError(
237
+ message=(f"enumeration for subject {current!r} truncated by remaining max_nodes budget"),
238
+ related=f"subject={current!r}",
239
+ )
240
+
241
+ if result.tuples and depth >= context.budget.max_depth:
242
+ raise EvaluationError(
243
+ message=(f"max_depth={context.budget.max_depth!r} exceeded during reverse lookup from {current!r}"),
244
+ related=f"subject={current!r}",
245
+ )
246
+
247
+ for tuple_ in sorted(result.tuples, key=repr):
248
+ candidates.add(tuple_.object)
249
+ for next_subject in self._reverse_frontier(tuple_.object):
250
+ if next_subject not in visited:
251
+ visited.add(next_subject)
252
+ queue.append((next_subject, depth + 1))
253
+
254
+ return frozenset(candidates)
255
+
256
+ @cached_property
257
+ def _referenceable_shapes(self) -> frozenset[str]:
258
+ """Return the subject shapes referenced by any relation in the model.
259
+
260
+ Returns:
261
+ frozenset[str]: Subject type and type/relation shapes allowed by model relations.
262
+
263
+ Raises:
264
+ None.
265
+ """
266
+ return frozenset(
267
+ allowed_shape
268
+ for type_declaration in self._model.types.values()
269
+ for relation in type_declaration.relations.values()
270
+ for allowed_shape in relation.allowed_subject_types
271
+ )
272
+
273
+ def _reverse_frontier(self, object: ObjectReference) -> tuple[Subject, ...]:
274
+ """Return the declared subject identities that may reference `object`.
275
+
276
+ Args:
277
+ object (ObjectReference): The object whose possible subject identities are generated.
278
+
279
+ Returns:
280
+ tuple[Subject, ...]: Direct and userset identities that model relations may reference.
281
+
282
+ Raises:
283
+ None.
284
+ """
285
+ next_subjects: list[Subject] = []
286
+ if object.type in self._referenceable_shapes:
287
+ next_subjects.append(DirectSubject(object=object))
288
+
289
+ declaration = self._model.types.get(object.type)
290
+ if declaration is None:
291
+ return tuple(next_subjects)
292
+
293
+ names = (*declaration.relations, *declaration.permissions)
294
+ next_subjects.extend(
295
+ [
296
+ SubjectSet(object=object, relation=name)
297
+ for name in names
298
+ if f"{object.type}#{name}" in self._referenceable_shapes
299
+ ]
300
+ )
301
+ return tuple(next_subjects)
302
+
151
303
  def _check_name(self, subject: Subject, name: str, object: ObjectReference, context: EvaluationContext) -> bool:
152
304
  """
153
305
  Dispatches (object, name) to a relation or permission check, with cycle/memo/budget bookkeeping.
@@ -116,3 +116,24 @@ class View(ABC):
116
116
  `limit + 1` matching tuples before returning — that would violate the contract.
117
117
  """
118
118
  raise NotImplementedError
119
+
120
+ @abstractmethod
121
+ def read_tuples_for_subject(self, subject: Subject, limit: int) -> ReadResult:
122
+ """
123
+ Enumerates every tuple whose subject exactly matches `subject`, bounded by `limit`.
124
+
125
+ Unlike `read_relation`, this is not filtered by relation name or
126
+ object; callers needing narrower results filter returned tuples.
127
+ See `ReadResult` for the `complete=False` contract.
128
+
129
+ Args:
130
+ subject (Subject): The exact subject to match.
131
+ limit (int): The maximum number of matching tuples to return.
132
+
133
+ Returns:
134
+ ReadResult: Matching tuples and whether the enumeration is complete.
135
+
136
+ Raises:
137
+ None: The abstract contract defines no exception of its own.
138
+ """
139
+ raise NotImplementedError
@@ -0,0 +1,5 @@
1
+ -- Index supporting View.read_tuples_for_subject(). Without it, a query
2
+ -- filtered only by the subject columns has no index to use — the
3
+ -- primary key is ordered object-first.
4
+ CREATE INDEX IF NOT EXISTS authz_tuples_subject_idx
5
+ ON "authz_tuples" (subject_type, subject_id, subject_relation);
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "policy-pattern"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "A minimal, embeddable ReBAC authorization engine for Python, inspired by Google Zanzibar."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -38,7 +38,7 @@ Repository = "https://github.com/guillem-ps/policy-pattern"
38
38
  Issues = "https://github.com/guillem-ps/policy-pattern/issues"
39
39
 
40
40
  [build-system]
41
- requires = ["uv_build>=0.11.16,<0.13.0"]
41
+ requires = ["uv_build>=0.11.16,<0.14.0"]
42
42
  build-backend = "uv_build"
43
43
 
44
44
  [tool.uv.build-backend]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "policy-pattern"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "A minimal, embeddable ReBAC authorization engine for Python, inspired by Google Zanzibar."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -37,7 +37,7 @@ Repository = "https://github.com/guillem-ps/policy-pattern"
37
37
  Issues = "https://github.com/guillem-ps/policy-pattern/issues"
38
38
 
39
39
  [build-system]
40
- requires = ["uv_build>=0.11.16,<0.13.0"]
40
+ requires = ["uv_build>=0.11.16,<0.14.0"]
41
41
  build-backend = "uv_build"
42
42
 
43
43
  [tool.uv.build-backend]