pyspecification 2.0.0__tar.gz → 2.0.2__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.
- {pyspecification-2.0.0 → pyspecification-2.0.2}/PKG-INFO +135 -1
- {pyspecification-2.0.0 → pyspecification-2.0.2}/README.md +134 -0
- {pyspecification-2.0.0 → pyspecification-2.0.2}/pyproject.toml +1 -1
- {pyspecification-2.0.0 → pyspecification-2.0.2}/pyproject.toml.orig +1 -1
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/registry.py +6 -6
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/rules.py +1 -1
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/__init__.py +0 -0
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/compilers.py +0 -0
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/exceptions.py +0 -0
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/json_schema.py +0 -0
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/predicate.py +0 -0
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/processors.py +0 -0
- {pyspecification-2.0.0 → pyspecification-2.0.2}/src/pyspecification/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyspecification
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A lightweight, typed Python library for composing business rules as reusable, executable predicates.
|
|
5
5
|
Author: aaltatan
|
|
6
6
|
Author-email: aaltatan <a.altatan@gmail.com>
|
|
@@ -354,6 +354,140 @@ This pattern is especially useful when you want:
|
|
|
354
354
|
|
|
355
355
|
---
|
|
356
356
|
|
|
357
|
+
### 6. Django ORM integration example
|
|
358
|
+
|
|
359
|
+
This example demonstrates how to use `pyspecification` with Django models.
|
|
360
|
+
`pyspecification` does not import Django or provide a Django-specific adapter;
|
|
361
|
+
the rules return Django `Q` objects, while the existing bitwise predicate
|
|
362
|
+
composition supplies `&`, `|`, and `~` support.
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
# models.py
|
|
366
|
+
from typing import Any, Self
|
|
367
|
+
|
|
368
|
+
from django.db import models
|
|
369
|
+
from pyspecification import Predicate
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class EmployeeQuerySet(models.QuerySet):
|
|
373
|
+
def filter_by_rule(self, rule: Predicate[Any, models.Q]) -> Self:
|
|
374
|
+
return self.filter(rule(Employee))
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class EmployeeManager(models.Manager):
|
|
378
|
+
def get_queryset(self) -> EmployeeQuerySet:
|
|
379
|
+
return EmployeeQuerySet(self.model, using=self._db)
|
|
380
|
+
|
|
381
|
+
def filter_by_rule(self, rule: Predicate[Any, models.Q]) -> EmployeeQuerySet:
|
|
382
|
+
return self.get_queryset().filter_by_rule(rule)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class Employee(models.Model):
|
|
386
|
+
name = models.CharField(max_length=100)
|
|
387
|
+
age = models.IntegerField()
|
|
388
|
+
is_admin = models.BooleanField(default=False)
|
|
389
|
+
|
|
390
|
+
objects = EmployeeManager()
|
|
391
|
+
|
|
392
|
+
def __str__(self) -> str:
|
|
393
|
+
return self.name
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
# rules.py
|
|
398
|
+
from typing import Any
|
|
399
|
+
|
|
400
|
+
from pyspecification import ObjectRulesRegistry
|
|
401
|
+
from django.db.models import Q
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
registry = ObjectRulesRegistry[Any, Q](operator="bitwise")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@registry.rule()
|
|
408
|
+
def is_admin(_: Any) -> Q:
|
|
409
|
+
return Q(is_admin=True)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@registry.rule()
|
|
413
|
+
def name__eq(_: Any, value: str) -> Q:
|
|
414
|
+
return Q(name=value)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@registry.rule()
|
|
418
|
+
def age__gte(_: Any, value: int) -> Q:
|
|
419
|
+
return Q(age__gte=value)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@registry.rule()
|
|
423
|
+
def age__lte(_: Any, value: int) -> Q:
|
|
424
|
+
return Q(age__lte=value)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
# views.py
|
|
429
|
+
from typing import Any
|
|
430
|
+
|
|
431
|
+
from django.db.models import Q
|
|
432
|
+
from django.http import HttpRequest, HttpResponse
|
|
433
|
+
from pyspecification import Predicate, PredicateCompiler
|
|
434
|
+
|
|
435
|
+
from .rules import registry
|
|
436
|
+
from .models import Employee
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
FILTER_RULE = {
|
|
440
|
+
"operator": "any",
|
|
441
|
+
"expressions": [
|
|
442
|
+
{
|
|
443
|
+
"name": "is_admin",
|
|
444
|
+
"args": [],
|
|
445
|
+
"kwargs": {},
|
|
446
|
+
"inverse": False,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
"operator": "all",
|
|
450
|
+
"expressions": [
|
|
451
|
+
{
|
|
452
|
+
"name": "name__eq",
|
|
453
|
+
"args": ["admin"],
|
|
454
|
+
"kwargs": {},
|
|
455
|
+
"inverse": False,
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"name": "age__gte",
|
|
459
|
+
"args": [18],
|
|
460
|
+
"kwargs": {},
|
|
461
|
+
"inverse": False,
|
|
462
|
+
},
|
|
463
|
+
]
|
|
464
|
+
}
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
compiler = PredicateCompiler[Any, Q](
|
|
469
|
+
registry.rules,
|
|
470
|
+
lambda _: Predicate(lambda _: Q(), operator="bitwise"),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def filter_employees(request: HttpRequest) -> HttpResponse:
|
|
474
|
+
predicate = compiler.compile(FILTER_RULE)
|
|
475
|
+
employees = Employee.objects.filter_by_rule(predicate)
|
|
476
|
+
return HttpResponse(f"Employees: {employees}")
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The model, rule, and view snippets can live in their normal Django app
|
|
480
|
+
modules. No Django settings or model changes are required beyond the standard
|
|
481
|
+
Django project setup.
|
|
482
|
+
|
|
483
|
+
This pattern is especially useful when you want:
|
|
484
|
+
|
|
485
|
+
- declarative Django queryset filters
|
|
486
|
+
- reusable admin and dashboard filter rules
|
|
487
|
+
- object-level permissions backed by Django `Q` expressions
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
357
491
|
## Custom processors
|
|
358
492
|
|
|
359
493
|
Rules can apply argument processors to coerce values before evaluation.
|
|
@@ -340,6 +340,140 @@ This pattern is especially useful when you want:
|
|
|
340
340
|
|
|
341
341
|
---
|
|
342
342
|
|
|
343
|
+
### 6. Django ORM integration example
|
|
344
|
+
|
|
345
|
+
This example demonstrates how to use `pyspecification` with Django models.
|
|
346
|
+
`pyspecification` does not import Django or provide a Django-specific adapter;
|
|
347
|
+
the rules return Django `Q` objects, while the existing bitwise predicate
|
|
348
|
+
composition supplies `&`, `|`, and `~` support.
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
# models.py
|
|
352
|
+
from typing import Any, Self
|
|
353
|
+
|
|
354
|
+
from django.db import models
|
|
355
|
+
from pyspecification import Predicate
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class EmployeeQuerySet(models.QuerySet):
|
|
359
|
+
def filter_by_rule(self, rule: Predicate[Any, models.Q]) -> Self:
|
|
360
|
+
return self.filter(rule(Employee))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class EmployeeManager(models.Manager):
|
|
364
|
+
def get_queryset(self) -> EmployeeQuerySet:
|
|
365
|
+
return EmployeeQuerySet(self.model, using=self._db)
|
|
366
|
+
|
|
367
|
+
def filter_by_rule(self, rule: Predicate[Any, models.Q]) -> EmployeeQuerySet:
|
|
368
|
+
return self.get_queryset().filter_by_rule(rule)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class Employee(models.Model):
|
|
372
|
+
name = models.CharField(max_length=100)
|
|
373
|
+
age = models.IntegerField()
|
|
374
|
+
is_admin = models.BooleanField(default=False)
|
|
375
|
+
|
|
376
|
+
objects = EmployeeManager()
|
|
377
|
+
|
|
378
|
+
def __str__(self) -> str:
|
|
379
|
+
return self.name
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
# rules.py
|
|
384
|
+
from typing import Any
|
|
385
|
+
|
|
386
|
+
from pyspecification import ObjectRulesRegistry
|
|
387
|
+
from django.db.models import Q
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
registry = ObjectRulesRegistry[Any, Q](operator="bitwise")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@registry.rule()
|
|
394
|
+
def is_admin(_: Any) -> Q:
|
|
395
|
+
return Q(is_admin=True)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@registry.rule()
|
|
399
|
+
def name__eq(_: Any, value: str) -> Q:
|
|
400
|
+
return Q(name=value)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@registry.rule()
|
|
404
|
+
def age__gte(_: Any, value: int) -> Q:
|
|
405
|
+
return Q(age__gte=value)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@registry.rule()
|
|
409
|
+
def age__lte(_: Any, value: int) -> Q:
|
|
410
|
+
return Q(age__lte=value)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
# views.py
|
|
415
|
+
from typing import Any
|
|
416
|
+
|
|
417
|
+
from django.db.models import Q
|
|
418
|
+
from django.http import HttpRequest, HttpResponse
|
|
419
|
+
from pyspecification import Predicate, PredicateCompiler
|
|
420
|
+
|
|
421
|
+
from .rules import registry
|
|
422
|
+
from .models import Employee
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
FILTER_RULE = {
|
|
426
|
+
"operator": "any",
|
|
427
|
+
"expressions": [
|
|
428
|
+
{
|
|
429
|
+
"name": "is_admin",
|
|
430
|
+
"args": [],
|
|
431
|
+
"kwargs": {},
|
|
432
|
+
"inverse": False,
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"operator": "all",
|
|
436
|
+
"expressions": [
|
|
437
|
+
{
|
|
438
|
+
"name": "name__eq",
|
|
439
|
+
"args": ["admin"],
|
|
440
|
+
"kwargs": {},
|
|
441
|
+
"inverse": False,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"name": "age__gte",
|
|
445
|
+
"args": [18],
|
|
446
|
+
"kwargs": {},
|
|
447
|
+
"inverse": False,
|
|
448
|
+
},
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
compiler = PredicateCompiler[Any, Q](
|
|
455
|
+
registry.rules,
|
|
456
|
+
lambda _: Predicate(lambda _: Q(), operator="bitwise"),
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def filter_employees(request: HttpRequest) -> HttpResponse:
|
|
460
|
+
predicate = compiler.compile(FILTER_RULE)
|
|
461
|
+
employees = Employee.objects.filter_by_rule(predicate)
|
|
462
|
+
return HttpResponse(f"Employees: {employees}")
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
The model, rule, and view snippets can live in their normal Django app
|
|
466
|
+
modules. No Django settings or model changes are required beyond the standard
|
|
467
|
+
Django project setup.
|
|
468
|
+
|
|
469
|
+
This pattern is especially useful when you want:
|
|
470
|
+
|
|
471
|
+
- declarative Django queryset filters
|
|
472
|
+
- reusable admin and dashboard filter rules
|
|
473
|
+
- object-level permissions backed by Django `Q` expressions
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
343
477
|
## Custom processors
|
|
344
478
|
|
|
345
479
|
Rules can apply argument processors to coerce values before evaluation.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyspecification"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.2"
|
|
4
4
|
description = "A lightweight, typed Python library for composing business rules as reusable, executable predicates."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "aaltatan", email = "a.altatan@gmail.com" }]
|
|
@@ -172,12 +172,12 @@ class ObjectRulesRegistry[T, R: ReturnType]:
|
|
|
172
172
|
) -> ObjectRuleFn[T, R, P]:
|
|
173
173
|
rule_name = _process_rule_name(fn, name)
|
|
174
174
|
|
|
175
|
-
if hidden:
|
|
176
|
-
self._hidden.add(rule_name)
|
|
177
|
-
|
|
178
175
|
if rule_name in self._rules:
|
|
179
176
|
raise RuleAlreadyRegisteredError(rule_name)
|
|
180
177
|
|
|
178
|
+
if hidden:
|
|
179
|
+
self._hidden.add(rule_name)
|
|
180
|
+
|
|
181
181
|
@wraps(fn)
|
|
182
182
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Predicate[T, R]:
|
|
183
183
|
processed_args, processed_kwargs = process_arguments(processors, *args, **kwargs)
|
|
@@ -364,12 +364,12 @@ class SubscriptableRulesRegistry[T, K, R: ReturnType]:
|
|
|
364
364
|
) -> SubscriptableRuleFn[T, K, R, P]:
|
|
365
365
|
rule_name = _process_rule_name(fn, name)
|
|
366
366
|
|
|
367
|
-
if hidden:
|
|
368
|
-
self._hidden.add(rule_name)
|
|
369
|
-
|
|
370
367
|
if rule_name in self._rules:
|
|
371
368
|
raise RuleAlreadyRegisteredError(rule_name)
|
|
372
369
|
|
|
370
|
+
if hidden:
|
|
371
|
+
self._hidden.add(rule_name)
|
|
372
|
+
|
|
373
373
|
@wraps(fn)
|
|
374
374
|
def wrapper(key: K, *args: P.args, **kwargs: P.kwargs) -> Predicate[T, R]:
|
|
375
375
|
|
|
@@ -168,7 +168,7 @@ def subscriptable_rule[T, K, R: ReturnType, **P](
|
|
|
168
168
|
check_key_existence
|
|
169
169
|
and isinstance(obj, list)
|
|
170
170
|
and isinstance(key, int)
|
|
171
|
-
and len(obj) <= key
|
|
171
|
+
and not (-len(obj) <= key < len(obj))
|
|
172
172
|
),
|
|
173
173
|
lambda: check_key_existence and isinstance(obj, dict) and key not in obj,
|
|
174
174
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|