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 +14 -0
- zspec/specification.py +111 -0
- zspec-0.1.0.dist-info/METADATA +79 -0
- zspec-0.1.0.dist-info/RECORD +5 -0
- zspec-0.1.0.dist-info/WHEEL +4 -0
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,,
|