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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyspecification
3
- Version: 2.0.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.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
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyspecification"
3
- version = "2.0.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
  ]