Skip to content

Implementation of Criteria patterns in Python for Complex Heart SDK.

License

Notifications You must be signed in to change notification settings

ComplexHeart/py-criteria

Repository files navigation

Criteria (a.k.a Filter)

Test Quality Gate Status Coverage

Small implementation of a filter criteria pattern in Python for Complex Heart SDK. Query-system agnostic - works with SQL, NoSQL, APIs, or any data source.

Installation

pip install complex-heart-criteria

or using uv:

uv add complex-heart-criteria

Usage

from complexheart.domain.criteria import Criteria, Filter, FilterGroup, Order, Page

Building FilterGroups

Use + to compose filters into a FilterGroup (AND logic within group):

active_adults = Filter.equal("status", "active") + Filter.greater_or_equal_than("age", 18)

Or use the fluent API:

active_adults = (
    FilterGroup.empty()
    .add_filter_equal("status", "active")
    .add_filter_greater_or_equal_than("age", 18)
)

Building Criteria

Add FilterGroups to Criteria (OR logic between groups):

c = (
    Criteria()
    .with_filter_group(active_adults)
    .with_order(Order.desc(("created_at",)))
    .with_page_limit(10)
    .with_page_offset(20)
)

Merging Criteria

Use | for OR and & for AND between Criteria objects:

active_users = Criteria().with_filter_group(
    Filter.equal("status", "active") + Filter.greater_than("age", 18)
)
admin_users = Criteria().with_filter_group(
    FilterGroup.create(Filter.equal("role", "admin"))
)

# OR: concatenate groups
combined = active_users | admin_users
# Result: (status = 'active' AND age > 18) OR (role = 'admin')

# AND: merge into single group
merged = active_users & admin_users
# Result: (status = 'active' AND age > 18 AND role = 'admin')

Fluent API

criteria = (
    Criteria()
    .filter("status", "==", "active", group=0)
    .filter("age", ">=", 18, group=0)
    .filter("role", "==", "admin", group=1)
    .order_by(("created_at",), "DESC")
    .limit(10)
    .offset(20)
)
# Result: (status = 'active' AND age >= 18) OR (role = 'admin')

Filter Factories

Filter.equal("name", "Vincent")              # name == 'Vincent'
Filter.not_equal("name", "Vincent")          # name != 'Vincent'
Filter.greater_than("age", 18)               # age > 18
Filter.greater_or_equal_than("age", 18)      # age >= 18
Filter.less_than("age", 65)                  # age < 65
Filter.less_or_equal_than("age", 65)         # age <= 65
Filter.in_("status", ["a", "b"])             # status IN ('a', 'b')
Filter.not_in("status", ["x"])               # status NOT IN ('x')
Filter.like("name", "Vin%")                  # name LIKE 'Vin%'
Filter.not_like("name", "Vin%")              # name NOT LIKE 'Vin%'
Filter.contains("tags", "vip")               # tags contains 'vip'
Filter.not_contains("tags", "x")             # tags not contains 'x'

FilterGroup Methods

group = (
    FilterGroup.empty()
    .add_filter_equal("status", "active")
    .add_filter_greater_than("age", 18)
    .add_filter_in("role", ["admin", "moderator"])
)

Available methods: add_filter_equal, add_filter_not_equal, add_filter_greater_than, add_filter_greater_or_equal_than, add_filter_less_than, add_filter_less_or_equal_than, add_filter_in, add_filter_not_in, add_filter_like, add_filter_not_like, add_filter_contains, add_filter_not_contains.

Repository Integration

customers = customer_repository.match(criteria)

Immutability

All classes are immutable frozen dataclasses. Methods return new instances:

c1 = Criteria()
c2 = c1.filter("name", "==", "Vincent")

assert c1 is not c2
assert len(c1.filters) == 0
assert len(c2.filters) == 1

Migration from v0.x

v1.0 introduces breaking changes:

Immutability

# Old (v0.x) - mutation pattern
c = Criteria()
c.filter("name", "==", "Vincent")  # mutated c

# New (v1.x) - immutable, must reassign
c = Criteria()
c = c.filter("name", "==", "Vincent")  # returns new instance

Filter Factory Names

# Old
Filter.eq("name", "v")
Filter.neq("name", "v")
Filter.gt("age", 18)
Filter.gte("age", 18)
Filter.lt("age", 65)
Filter.lte("age", 65)

# New (aligned with PHP version)
Filter.equal("name", "v")
Filter.not_equal("name", "v")
Filter.greater_than("age", 18)
Filter.greater_or_equal_than("age", 18)
Filter.less_than("age", 65)
Filter.less_or_equal_than("age", 65)

Order.by is now tuple

# Old
Order.desc(["name", "age"])

# New
Order.desc(("name", "age"))

Criteria operators only accept Criteria

# Old - operators accepted Filter/FilterGroup
c = Criteria() | Filter.equal("a", 1)

# New - use with_filter_group instead
c = Criteria().with_filter_group(FilterGroup.create(Filter.equal("a", 1)))

# Operators only work between Criteria objects
c1 = Criteria().with_filter_group(group1)
c2 = Criteria().with_filter_group(group2)
combined = c1 | c2  # OK
merged = c1 & c2    # OK

About

Implementation of Criteria patterns in Python for Complex Heart SDK.

Topics

Resources

License

Stars

Watchers

Forks

Languages