koalify 0.2.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.
- koalify-0.2.0/LICENSE +21 -0
- koalify-0.2.0/PKG-INFO +137 -0
- koalify-0.2.0/README.md +118 -0
- koalify-0.2.0/koalify/__init__.py +13 -0
- koalify-0.2.0/koalify/comparisons.py +89 -0
- koalify-0.2.0/koalify/criteria.py +71 -0
- koalify-0.2.0/koalify/fields.py +79 -0
- koalify-0.2.0/pyproject.toml +18 -0
koalify-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dragos Dumitrache
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
koalify-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: koalify
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A compact predicate DSL for matching criteria against any object
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Dragos Dumitrache
|
|
8
|
+
Author-email: dragos@afterburner.dev
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# koalify
|
|
20
|
+
|
|
21
|
+
A compact predicate DSL for matching criteria against any Python object. Zero runtime dependencies.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install koalify
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from koalify import F, all_of, any_of
|
|
33
|
+
|
|
34
|
+
# Build rules with Python operators
|
|
35
|
+
is_eligible = (
|
|
36
|
+
(F.status == "active")
|
|
37
|
+
& (F.age >= 18)
|
|
38
|
+
& (F.role.in_({"admin", "moderator", "editor"}))
|
|
39
|
+
& F.score.between(50, 100)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Evaluate against any object with attributes
|
|
43
|
+
is_eligible(user) # True / False
|
|
44
|
+
|
|
45
|
+
# Nested fields
|
|
46
|
+
lives_in_london = F.address.city == "London"
|
|
47
|
+
|
|
48
|
+
# Compose with OR / NOT
|
|
49
|
+
can_access = is_eligible | (lives_in_london & ~(F.status == "banned"))
|
|
50
|
+
|
|
51
|
+
# Dynamic composition from a list
|
|
52
|
+
conditions = [F.status == "active", F.age >= 18]
|
|
53
|
+
rule = all_of(*conditions)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
### Dataclasses
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from dataclasses import dataclass
|
|
62
|
+
from koalify import F, all_of
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Order:
|
|
66
|
+
product: str
|
|
67
|
+
quantity: int
|
|
68
|
+
price: float
|
|
69
|
+
fulfilled: bool
|
|
70
|
+
|
|
71
|
+
needs_review = (F.quantity > 100) & (F.price >= 500) & (F.fulfilled == False)
|
|
72
|
+
|
|
73
|
+
order = Order(product="Widget", quantity=200, price=750.0, fulfilled=False)
|
|
74
|
+
needs_review(order) # True
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Pydantic
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from pydantic import BaseModel
|
|
81
|
+
from koalify import F, any_of
|
|
82
|
+
|
|
83
|
+
class Address(BaseModel):
|
|
84
|
+
city: str
|
|
85
|
+
country: str
|
|
86
|
+
|
|
87
|
+
class Customer(BaseModel):
|
|
88
|
+
name: str
|
|
89
|
+
tier: str
|
|
90
|
+
address: Address
|
|
91
|
+
|
|
92
|
+
is_priority = (F.tier.in_({"gold", "platinum"})) | (F.address.country == "US")
|
|
93
|
+
|
|
94
|
+
customer = Customer(name="Alice", tier="gold", address=Address(city="London", country="UK"))
|
|
95
|
+
is_priority(customer) # True
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Dynamic rule composition
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from koalify import F, all_of
|
|
102
|
+
|
|
103
|
+
def build_filter(min_age: int | None = None, status: str | None = None, roles: set[str] | None = None):
|
|
104
|
+
criteria = []
|
|
105
|
+
if min_age is not None:
|
|
106
|
+
criteria.append(F.age >= min_age)
|
|
107
|
+
if status is not None:
|
|
108
|
+
criteria.append(F.status == status)
|
|
109
|
+
if roles is not None:
|
|
110
|
+
criteria.append(F.role.in_(roles))
|
|
111
|
+
return all_of(*criteria) if criteria else lambda _: True
|
|
112
|
+
|
|
113
|
+
user_filter = build_filter(min_age=18, roles={"admin", "editor"})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
| Symbol | Description |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `F.field` | Reference a field (supports nesting: `F.a.b.c`) |
|
|
121
|
+
| `== != > >= < <=` | Comparison operators on `FieldRef` |
|
|
122
|
+
| `.in_(values)` | Set membership |
|
|
123
|
+
| `.between(lo, hi)` | Inclusive range check |
|
|
124
|
+
| `&` | AND (flattens nested ANDs) |
|
|
125
|
+
| `\|` | OR (flattens nested ORs) |
|
|
126
|
+
| `~` | NOT |
|
|
127
|
+
| `all_of(*criteria)` | AND from a list |
|
|
128
|
+
| `any_of(*criteria)` | OR from a list |
|
|
129
|
+
|
|
130
|
+
## How It Works
|
|
131
|
+
|
|
132
|
+
`F.field_name` returns a `FieldRef`. Comparison operators on `FieldRef` produce `Criterion` objects. Criteria compose with `&`, `|`, and `~`. Calling a criterion resolves field values via `getattr` — works with dataclasses, Pydantic models, namedtuples, or any object with attributes.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
137
|
+
|
koalify-0.2.0/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# koalify
|
|
2
|
+
|
|
3
|
+
A compact predicate DSL for matching criteria against any Python object. Zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install koalify
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from koalify import F, all_of, any_of
|
|
15
|
+
|
|
16
|
+
# Build rules with Python operators
|
|
17
|
+
is_eligible = (
|
|
18
|
+
(F.status == "active")
|
|
19
|
+
& (F.age >= 18)
|
|
20
|
+
& (F.role.in_({"admin", "moderator", "editor"}))
|
|
21
|
+
& F.score.between(50, 100)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Evaluate against any object with attributes
|
|
25
|
+
is_eligible(user) # True / False
|
|
26
|
+
|
|
27
|
+
# Nested fields
|
|
28
|
+
lives_in_london = F.address.city == "London"
|
|
29
|
+
|
|
30
|
+
# Compose with OR / NOT
|
|
31
|
+
can_access = is_eligible | (lives_in_london & ~(F.status == "banned"))
|
|
32
|
+
|
|
33
|
+
# Dynamic composition from a list
|
|
34
|
+
conditions = [F.status == "active", F.age >= 18]
|
|
35
|
+
rule = all_of(*conditions)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Examples
|
|
39
|
+
|
|
40
|
+
### Dataclasses
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from koalify import F, all_of
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Order:
|
|
48
|
+
product: str
|
|
49
|
+
quantity: int
|
|
50
|
+
price: float
|
|
51
|
+
fulfilled: bool
|
|
52
|
+
|
|
53
|
+
needs_review = (F.quantity > 100) & (F.price >= 500) & (F.fulfilled == False)
|
|
54
|
+
|
|
55
|
+
order = Order(product="Widget", quantity=200, price=750.0, fulfilled=False)
|
|
56
|
+
needs_review(order) # True
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Pydantic
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pydantic import BaseModel
|
|
63
|
+
from koalify import F, any_of
|
|
64
|
+
|
|
65
|
+
class Address(BaseModel):
|
|
66
|
+
city: str
|
|
67
|
+
country: str
|
|
68
|
+
|
|
69
|
+
class Customer(BaseModel):
|
|
70
|
+
name: str
|
|
71
|
+
tier: str
|
|
72
|
+
address: Address
|
|
73
|
+
|
|
74
|
+
is_priority = (F.tier.in_({"gold", "platinum"})) | (F.address.country == "US")
|
|
75
|
+
|
|
76
|
+
customer = Customer(name="Alice", tier="gold", address=Address(city="London", country="UK"))
|
|
77
|
+
is_priority(customer) # True
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Dynamic rule composition
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from koalify import F, all_of
|
|
84
|
+
|
|
85
|
+
def build_filter(min_age: int | None = None, status: str | None = None, roles: set[str] | None = None):
|
|
86
|
+
criteria = []
|
|
87
|
+
if min_age is not None:
|
|
88
|
+
criteria.append(F.age >= min_age)
|
|
89
|
+
if status is not None:
|
|
90
|
+
criteria.append(F.status == status)
|
|
91
|
+
if roles is not None:
|
|
92
|
+
criteria.append(F.role.in_(roles))
|
|
93
|
+
return all_of(*criteria) if criteria else lambda _: True
|
|
94
|
+
|
|
95
|
+
user_filter = build_filter(min_age=18, roles={"admin", "editor"})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
| Symbol | Description |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `F.field` | Reference a field (supports nesting: `F.a.b.c`) |
|
|
103
|
+
| `== != > >= < <=` | Comparison operators on `FieldRef` |
|
|
104
|
+
| `.in_(values)` | Set membership |
|
|
105
|
+
| `.between(lo, hi)` | Inclusive range check |
|
|
106
|
+
| `&` | AND (flattens nested ANDs) |
|
|
107
|
+
| `\|` | OR (flattens nested ORs) |
|
|
108
|
+
| `~` | NOT |
|
|
109
|
+
| `all_of(*criteria)` | AND from a list |
|
|
110
|
+
| `any_of(*criteria)` | OR from a list |
|
|
111
|
+
|
|
112
|
+
## How It Works
|
|
113
|
+
|
|
114
|
+
`F.field_name` returns a `FieldRef`. Comparison operators on `FieldRef` produce `Criterion` objects. Criteria compose with `&`, `|`, and `~`. Calling a criterion resolves field values via `getattr` — works with dataclasses, Pydantic models, namedtuples, or any object with attributes.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Leaf criteria: field-vs-value comparisons."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from koalify.criteria import Criterion
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _Compare(Criterion):
|
|
11
|
+
"""Base for field-vs-value comparisons."""
|
|
12
|
+
|
|
13
|
+
op: str
|
|
14
|
+
|
|
15
|
+
def __init__(self, field: Any, value: Any):
|
|
16
|
+
self.field = field
|
|
17
|
+
self.value = value
|
|
18
|
+
|
|
19
|
+
def __repr__(self) -> str:
|
|
20
|
+
return f"{self.field!r} {self.op} {self.value!r}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Eq(_Compare):
|
|
24
|
+
op = "=="
|
|
25
|
+
|
|
26
|
+
def match(self, obj: Any) -> bool:
|
|
27
|
+
return self.field.resolve(obj) == self.value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Ne(_Compare):
|
|
31
|
+
op = "!="
|
|
32
|
+
|
|
33
|
+
def match(self, obj: Any) -> bool:
|
|
34
|
+
return self.field.resolve(obj) != self.value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Gt(_Compare):
|
|
38
|
+
op = ">"
|
|
39
|
+
|
|
40
|
+
def match(self, obj: Any) -> bool:
|
|
41
|
+
return self.field.resolve(obj) > self.value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Ge(_Compare):
|
|
45
|
+
op = ">="
|
|
46
|
+
|
|
47
|
+
def match(self, obj: Any) -> bool:
|
|
48
|
+
return self.field.resolve(obj) >= self.value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Lt(_Compare):
|
|
52
|
+
op = "<"
|
|
53
|
+
|
|
54
|
+
def match(self, obj: Any) -> bool:
|
|
55
|
+
return self.field.resolve(obj) < self.value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Le(_Compare):
|
|
59
|
+
op = "<="
|
|
60
|
+
|
|
61
|
+
def match(self, obj: Any) -> bool:
|
|
62
|
+
return self.field.resolve(obj) <= self.value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class In(Criterion):
|
|
66
|
+
def __init__(self, field: Any, values: set | frozenset | list | tuple):
|
|
67
|
+
self.field = field
|
|
68
|
+
self.values = values
|
|
69
|
+
|
|
70
|
+
def match(self, obj: Any) -> bool:
|
|
71
|
+
return self.field.resolve(obj) in self.values
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"{self.field!r} in {self.values!r}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Between(Criterion):
|
|
78
|
+
"""Inclusive on both bounds: lower <= value <= upper."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, field: Any, lower: Any, upper: Any):
|
|
81
|
+
self.field = field
|
|
82
|
+
self.lower = lower
|
|
83
|
+
self.upper = upper
|
|
84
|
+
|
|
85
|
+
def match(self, obj: Any) -> bool:
|
|
86
|
+
return self.lower <= self.field.resolve(obj) <= self.upper
|
|
87
|
+
|
|
88
|
+
def __repr__(self) -> str:
|
|
89
|
+
return f"{self.lower!r} <= {self.field!r} <= {self.upper!r}"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Base criterion type and composite operators (AND, OR, NOT)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Criterion:
|
|
9
|
+
"""A composable predicate that can be evaluated against any object with attributes."""
|
|
10
|
+
|
|
11
|
+
def match(self, obj: Any) -> bool:
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
|
|
14
|
+
def __call__(self, obj: Any) -> bool:
|
|
15
|
+
return self.match(obj)
|
|
16
|
+
|
|
17
|
+
def __and__(self, other: Criterion) -> Criterion:
|
|
18
|
+
left = self.criteria if isinstance(self, And) else (self,)
|
|
19
|
+
right = other.criteria if isinstance(other, And) else (other,)
|
|
20
|
+
return And(*left, *right)
|
|
21
|
+
|
|
22
|
+
def __or__(self, other: Criterion) -> Criterion:
|
|
23
|
+
left = self.criteria if isinstance(self, Or) else (self,)
|
|
24
|
+
right = other.criteria if isinstance(other, Or) else (other,)
|
|
25
|
+
return Or(*left, *right)
|
|
26
|
+
|
|
27
|
+
def __invert__(self) -> Not:
|
|
28
|
+
return Not(self)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class And(Criterion):
|
|
32
|
+
def __init__(self, *criteria: Criterion):
|
|
33
|
+
self.criteria = criteria
|
|
34
|
+
|
|
35
|
+
def match(self, obj: Any) -> bool:
|
|
36
|
+
return all(c.match(obj) for c in self.criteria)
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return f"({' & '.join(repr(c) for c in self.criteria)})"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Or(Criterion):
|
|
43
|
+
def __init__(self, *criteria: Criterion):
|
|
44
|
+
self.criteria = criteria
|
|
45
|
+
|
|
46
|
+
def match(self, obj: Any) -> bool:
|
|
47
|
+
return any(c.match(obj) for c in self.criteria)
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return f"({' | '.join(repr(c) for c in self.criteria)})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Not(Criterion):
|
|
54
|
+
def __init__(self, criterion: Criterion):
|
|
55
|
+
self.criterion = criterion
|
|
56
|
+
|
|
57
|
+
def match(self, obj: Any) -> bool:
|
|
58
|
+
return not self.criterion.match(obj)
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
return f"~{self.criterion!r}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def all_of(*criteria: Criterion) -> And:
|
|
65
|
+
"""Combine criteria with AND (useful for dynamic / programmatic composition)."""
|
|
66
|
+
return And(*criteria)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def any_of(*criteria: Criterion) -> Or:
|
|
70
|
+
"""Combine criteria with OR (useful for dynamic / programmatic composition)."""
|
|
71
|
+
return Or(*criteria)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Field reference and accessor — the ``F.field_name`` entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from koalify.comparisons import Between, Eq, Ge, Gt, In, Le, Lt, Ne
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FieldRef:
|
|
11
|
+
"""
|
|
12
|
+
Reference to one (possibly nested) field on an object.
|
|
13
|
+
|
|
14
|
+
Supports Python comparison operators to produce criteria, and
|
|
15
|
+
attribute access for nested fields: F.address.city
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, *path: str):
|
|
19
|
+
self._path = path
|
|
20
|
+
|
|
21
|
+
def resolve(self, obj: Any) -> Any:
|
|
22
|
+
value: Any = obj
|
|
23
|
+
for part in self._path:
|
|
24
|
+
value = getattr(value, part)
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
# ── nested access ────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, name: str) -> FieldRef:
|
|
30
|
+
if name.startswith("_"):
|
|
31
|
+
raise AttributeError(name)
|
|
32
|
+
return FieldRef(*self._path, name)
|
|
33
|
+
|
|
34
|
+
# ── comparison operators → criteria ──────────────────────────
|
|
35
|
+
|
|
36
|
+
def __eq__(self, value: Any) -> Eq:
|
|
37
|
+
return Eq(self, value)
|
|
38
|
+
|
|
39
|
+
def __ne__(self, value: Any) -> Ne:
|
|
40
|
+
return Ne(self, value)
|
|
41
|
+
|
|
42
|
+
def __gt__(self, value: Any) -> Gt:
|
|
43
|
+
return Gt(self, value)
|
|
44
|
+
|
|
45
|
+
def __ge__(self, value: Any) -> Ge:
|
|
46
|
+
return Ge(self, value)
|
|
47
|
+
|
|
48
|
+
def __lt__(self, value: Any) -> Lt:
|
|
49
|
+
return Lt(self, value)
|
|
50
|
+
|
|
51
|
+
def __le__(self, value: Any) -> Le:
|
|
52
|
+
return Le(self, value)
|
|
53
|
+
|
|
54
|
+
# ── set / range predicates ───────────────────────────────────
|
|
55
|
+
|
|
56
|
+
def in_(self, values: set | frozenset | list | tuple) -> In:
|
|
57
|
+
return In(self, values)
|
|
58
|
+
|
|
59
|
+
def between(self, lower: Any, upper: Any) -> Between:
|
|
60
|
+
return Between(self, lower, upper)
|
|
61
|
+
|
|
62
|
+
# ── repr ─────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return ".".join(self._path)
|
|
66
|
+
|
|
67
|
+
def __hash__(self) -> int:
|
|
68
|
+
return hash(self._path)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _FieldAccessor:
|
|
72
|
+
"""Singleton entry-point: ``F.field_name`` creates a :class:`FieldRef`."""
|
|
73
|
+
|
|
74
|
+
def __getattr__(self, name: str) -> FieldRef:
|
|
75
|
+
return FieldRef(name)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
F = _FieldAccessor()
|
|
79
|
+
"""Use ``F.field_name`` to reference fields in criteria."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "koalify"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "A compact predicate DSL for matching criteria against any object"
|
|
5
|
+
authors = ["Dragos Dumitrache <dragos@afterburner.dev>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.10"
|
|
11
|
+
|
|
12
|
+
[tool.poetry.group.dev.dependencies]
|
|
13
|
+
pytest = "^8.0"
|
|
14
|
+
pydantic = "^2.0"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["poetry-core"]
|
|
18
|
+
build-backend = "poetry.core.masonry.api"
|