zspec 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.
zspec/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """ZSpec — a specification pattern library for Python."""
2
+
3
+ from zspec.specification import (
4
+ AndSpecification as AndSpecification,
5
+ )
6
+ from zspec.specification import (
7
+ NotSpecification as NotSpecification,
8
+ )
9
+ from zspec.specification import (
10
+ OrSpecification as OrSpecification,
11
+ )
12
+ from zspec.specification import (
13
+ Specification as Specification,
14
+ )
zspec/specification.py ADDED
@@ -0,0 +1,111 @@
1
+ """Specification pattern — composable business rule objects."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Iterable
5
+ from functools import reduce
6
+ from operator import and_, or_
7
+ from typing import override
8
+
9
+
10
+ class Specification[T](ABC):
11
+ """Abstract specification that can be combined with ``&``, ``|``, ``~``."""
12
+
13
+ __slots__: tuple[str, ...] = ()
14
+
15
+ @abstractmethod
16
+ def is_satisfied_by(self, candidate: T) -> bool:
17
+ """Check whether *candidate* satisfies this specification."""
18
+ raise NotImplementedError
19
+
20
+ def __and__(self, other: Specification[T]) -> AndSpecification[T]:
21
+ """Combine with *other* via logical AND."""
22
+ return AndSpecification(left=self, right=other)
23
+
24
+ def __or__(self, other: Specification[T]) -> OrSpecification[T]:
25
+ """Combine with *other* via logical OR."""
26
+ return OrSpecification(self, other)
27
+
28
+ def __invert__(self) -> NotSpecification[T]:
29
+ """Negate this specification."""
30
+ return NotSpecification(self)
31
+
32
+ def __call__(self, candidate: T) -> bool:
33
+ """Evaluate the specification against *candidate*."""
34
+ return self.is_satisfied_by(candidate)
35
+
36
+ @classmethod
37
+ def all_of(
38
+ cls, specs: Iterable[Specification[T]],
39
+ ) -> Specification[T] | None:
40
+ """Return a specification that is satisfied when **all** of *specs* are.
41
+
42
+ Returns ``None`` when *specs* is empty.
43
+ """
44
+ items = list(specs)
45
+ if not items:
46
+ return None
47
+ return reduce(and_, items)
48
+
49
+ @classmethod
50
+ def any_of(
51
+ cls, specs: Iterable[Specification[T]],
52
+ ) -> Specification[T] | None:
53
+ """Return a specification that is satisfied when **any** of *specs* is.
54
+
55
+ Returns ``None`` when *specs* is empty.
56
+ """
57
+ items = list(specs)
58
+ if not items:
59
+ return None
60
+ return reduce(or_, items)
61
+
62
+
63
+ class AndSpecification[T](Specification[T]):
64
+ """Conjunction of two specifications (produced by ``&``)."""
65
+
66
+ __slots__ = ("left", "right")
67
+
68
+ def __init__(self, left: Specification[T], right: Specification[T]) -> None:
69
+ """Initialize with *left* and *right* specifications."""
70
+ self.left = left
71
+ self.right = right
72
+
73
+ @override
74
+ def is_satisfied_by(self, candidate: T) -> bool:
75
+ """Check whether *candidate* satisfies both specifications."""
76
+ return self.left.is_satisfied_by(
77
+ candidate,
78
+ ) and self.right.is_satisfied_by(candidate)
79
+
80
+
81
+ class OrSpecification[T](Specification[T]):
82
+ """Disjunction of two specifications (produced by ``|``)."""
83
+
84
+ __slots__ = ("left", "right")
85
+
86
+ def __init__(self, left: Specification[T], right: Specification[T]) -> None:
87
+ """Initialize with *left* and *right* specifications."""
88
+ self.left = left
89
+ self.right = right
90
+
91
+ @override
92
+ def is_satisfied_by(self, candidate: T) -> bool:
93
+ """Check whether *candidate* satisfies at least one specification."""
94
+ return self.left.is_satisfied_by(
95
+ candidate,
96
+ ) or self.right.is_satisfied_by(candidate)
97
+
98
+
99
+ class NotSpecification[T](Specification[T]):
100
+ """Negation of a specification (produced by ``~``)."""
101
+
102
+ __slots__ = ("spec",)
103
+
104
+ def __init__(self, spec: Specification[T]) -> None:
105
+ """Initialize with *spec* to negate."""
106
+ self.spec = spec
107
+
108
+ @override
109
+ def is_satisfied_by(self, candidate: T) -> bool:
110
+ """Check whether *candidate* does **not** satisfy the specification."""
111
+ return not self.spec.is_satisfied_by(candidate)
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.3
2
+ Name: zspec
3
+ Version: 0.1.0
4
+ Summary: Composable specification pattern for Python
5
+ Author: Alexandr
6
+ Author-email: Alexandr <alexandr.panteleev2000@gmail.com>
7
+ Requires-Python: >=3.14
8
+ Description-Content-Type: text/markdown
9
+
10
+ # ZSpec
11
+
12
+ Composable specification pattern for Python 3.14+.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install zspec
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ from dataclasses import dataclass
24
+ from zspec import Specification
25
+
26
+
27
+ @dataclass
28
+ class Product:
29
+ name: str
30
+ price: int
31
+ in_stock: bool
32
+
33
+
34
+ # Define specifications as simple subclasses
35
+ class InStock(Specification[Product]):
36
+ def is_satisfied_by(self, p: Product) -> bool:
37
+ return p.in_stock
38
+
39
+
40
+ class Affordable(Specification[Product]):
41
+ def __init__(self, max_price: int) -> None:
42
+ self.max_price = max_price
43
+
44
+ def is_satisfied_by(self, p: Product) -> bool:
45
+ return p.price <= self.max_price
46
+
47
+
48
+ # Compose with &, |, ~
49
+ in_stock = InStock()
50
+ reasonable = Affordable(max_price=1000)
51
+ eligible = in_stock & reasonable
52
+
53
+ product = Product(name="Laptop", price=800, in_stock=True)
54
+ assert eligible(product) # True -- callable directly
55
+ assert eligible.is_satisfied_by(product) # same thing
56
+ ```
57
+
58
+ ## Features
59
+
60
+ - **Composable** --- combine specs with `&` (and), `|` (or), `~` (not)
61
+ - **Type-safe** --- generic `Specification[T]` preserves the candidate type
62
+ - **Bulk combinators** --- `Specification.all_of(...)` and `Specification.any_of(...)`
63
+ - **Zero dependencies** --- standard library only
64
+ - **Python 3.14+** --- leverages modern generics (`class Foo[T]`)
65
+
66
+ ## API overview
67
+
68
+ | Method | Description |
69
+ |---|---|
70
+ | `spec & other` | Both must be satisfied (AND) |
71
+ | `spec \| other` | At least one must be satisfied (OR) |
72
+ | `~spec` | Negation (NOT) |
73
+ | `spec(candidate)` | Shorthand for `is_satisfied_by` |
74
+ | `Specification.all_of(specs)` | Reduce with AND, returns `None` for empty input |
75
+ | `Specification.any_of(specs)` | Reduce with OR, returns `None` for empty input |
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,5 @@
1
+ zspec/__init__.py,sha256=0VSucegkSPaM3H0OWBMhbT0w9J-7Mry0x-afvz9DuhE,365
2
+ zspec/specification.py,sha256=LzgVVpgv1EzLK7BcmS3sJb5mtVEX23ZaltIYl9tl53k,3592
3
+ zspec-0.1.0.dist-info/WHEEL,sha256=iCTolw4aw2dP3yfM-EQCGTDsFCXL_ymmbYnBRVH7plA,81
4
+ zspec-0.1.0.dist-info/METADATA,sha256=TC_J72-rDeJ3FBRC85X4TMqTy8p6vaI2-jy12YMel4Q,1988
5
+ zspec-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.11
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any