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 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
+
@@ -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,13 @@
1
+ from koalify.criteria import And, Criterion, Not, Or, all_of, any_of
2
+ from koalify.fields import F, FieldRef
3
+
4
+ __all__ = [
5
+ "And",
6
+ "Criterion",
7
+ "F",
8
+ "FieldRef",
9
+ "Not",
10
+ "Or",
11
+ "all_of",
12
+ "any_of",
13
+ ]
@@ -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"