Skip to main content

A Python library for modeling queries, filters, expressions, grouping, and aggregations as object structures

Project description

Therismos

θερισμός

Greek; noun

Harvest.

A Python library for modeling queries, filters, expressions, grouping, and aggregations as object structures.

Features

  • Backend-agnostic modeling: Build expressions, filters, and aggregations independent of any specific backend
  • Declarative DSL: Natural Python syntax for building complex queries
  • Type safety: Optional field type declarations with automatic casting
  • Immutable structures: All nodes are immutable and thread-safe
  • Automatic normalization: Compound expressions are automatically flattened
  • Powerful optimizer: Detects contradictions, tautologies, and simplification opportunities
  • Grammar-based serialization: Convert expressions to/from compact strings for URLs and APIs
  • Visitor pattern: Extensible architecture for converting to any backend format
  • Optimization tracking: Optional tracking of all optimization transformations

Installation

pip install therismos

Or using uv:

uv pip install therismos

Expressions

Therismos provides a comprehensive expression system for modeling filters and conditions as object structures using an Abstract Syntax Tree (AST) approach.

Quick Start

from therismos import F, optimize

# Define fields
age = F("age", int)
name = F("name")
status = F("status")

# Build expressions using natural Python syntax
expr = (age > 18) & (name == "Alice") | (status == "admin")

# Optimize the expression
optimized, records = optimize(expr)

# More complex example: detect contradictions
contradiction = (age < 30) & (age > 40)
result, _ = optimize(contradiction)
# result is FALSE

# Aggregate OR equality chains
multi_status = (status == "active") | (status == "pending") | (status == "completed")
result, _ = optimize(multi_status)
# result is: status IN ("active", "pending", "completed")

Expression Types

Atomic Expressions

  • Comparisons: ==, !=, <, <=, >, >=
  • Regex matching: field.matches(pattern, flags=None)
  • Membership: field.is_in(*values) or field.is_one_of(iterable)
  • Null checking: field.is_null(), field.is_not_null()
  • Constants: TRUE, FALSE

Compound Expressions

  • AND: expr1 & expr2 or AllExpr(expr1, expr2, ...)
  • OR: expr1 | expr2 or AnyExpr(expr1, expr2, ...)
  • NOT: ~expr or NotExpr(expr)

Type Casting

Fields can declare expected types for automatic value casting:

age = F("age", int)
price = F("price", float)

# Values are automatically cast
expr = age == "42"  # value is stored as string
casted = expr.casted_value()  # returns integer 42

Custom cast functions are also supported:

def normalize_email(value):
    return str(value).strip().lower()

email = F("email", normalize_email)

Optimization

The optimizer applies various rules to simplify expressions and detect logical issues.

Basic Examples

from therismos import optimize, F, TRUE, FALSE, AllExpr, AnyExpr

age = F("age")

# Identity elimination
expr = AllExpr(age > 18, TRUE, age < 65)
result, _ = optimize(expr)
# result is: AllExpr(age > 18, age < 65)

# Contradiction detection
expr = (age == 25) & (age != 25)
result, _ = optimize(expr)
# result is: FALSE

# Tautology detection
expr = (age < 30) | (age >= 30)
result, _ = optimize(expr)
# result is: TRUE

# NOT simplification (De Morgan's laws)
expr = ~((age > 18) & (name == "Alice"))
result, _ = optimize(expr)
# result is: (age <= 18) OR (name != "Alice")

Optimization Rules Reference

The optimizer implements the following transformation rules:

Atomic Expression Simplifications
Rule Before After
Empty IN to FALSE f IN () FALSE
Single-value IN to Eq f IN (v) f == v
NOT Expression Simplifications
Rule Before After
NOT of TRUE NOT(TRUE) FALSE
NOT of FALSE NOT(FALSE) TRUE
Double negation NOT(NOT(x)) x
De Morgan's law (AND) NOT(a AND b) NOT(a) OR NOT(b)
De Morgan's law (OR) NOT(a OR b) NOT(a) AND NOT(b)
AND Expression Simplifications
Rule Before After
Empty AND AND() TRUE
Single operand AND(x) x
FALSE propagation AND(..., FALSE, ...) FALSE
TRUE elimination AND(..., TRUE, ...) AND(...) (TRUE removed)
All TRUE AND(TRUE, TRUE, ...) TRUE
Eq/Eq same value (f == v) AND (f == v) f == v
Eq/Eq different values (f == v1) AND (f == v2) FALSE
Eq/In intersection (member) (f == v) AND (f IN (v, ...)) f == v
Eq/In intersection (non-member) (f == v) AND (f IN (...)) FALSE (v not in set)
In/In intersection (empty) (f IN (v1, v2)) AND (f IN (v3, v4)) FALSE (no overlap)
In/In intersection (single) (f IN (v1, v2)) AND (f IN (v2, v3)) f == v2
In/In intersection (multiple) (f IN (v1, v2, v3)) AND (f IN (v2, v3, v4)) f IN (v2, v3)
OR Expression Simplifications
Rule Before After
Empty OR OR() FALSE
Single operand OR(x) x
TRUE propagation OR(..., TRUE, ...) TRUE
FALSE elimination OR(..., FALSE, ...) OR(...) (FALSE removed)
All FALSE OR(FALSE, FALSE, ...) FALSE
Eq/Eq union (f == v1) OR (f == v2) f IN (v1, v2)
Eq/In union (f == v) OR (f IN (v2, v3)) f IN (v, v2, v3)
In/In union (f IN (v1, v2)) OR (f IN (v3, v4)) f IN (v1, v2, v3, v4)
Contradiction Detection (AND)
Pattern Result
(f == v) AND (f != v) FALSE
f.is_null() AND f.is_not_null() FALSE
(f < a) AND (f > b) where b >= a FALSE
(f <= a) AND (f > a) FALSE
(f >= b) AND (f < b) FALSE
Tautology Detection (OR)
Pattern Result
(f == v) OR (f != v) TRUE
f.is_null() OR f.is_not_null() TRUE
(f < v) OR (f >= v) TRUE
(f <= v) OR (f > v) TRUE

Complex Real-World Example: Detecting Accidental Contradictions

The optimizer is particularly valuable for catching accidentally contradictory conditions in complex business logic. Here's a realistic scenario where multiple nested requirements create an impossible condition:

from therismos import F, optimize, FALSE

# Define fields
user_age = F("age", int)
user_role = F("role")
user_status = F("status")
account_tier = F("account_tier")
dept = F("department")
experience = F("experience_years", int)
available = F("available", bool)

# Complex filter built incrementally by different team members
# Each level seemed reasonable in isolation, but together they create a contradiction
complex_filter = (
    (
        # Level 1: Nested OR conditions for base eligibility
        (
            (
                # Premium account holders
                (account_tier == "premium") &
                (
                    (user_role == "developer") |
                    (user_role == "designer")
                )
            ) |
            (
                # OR enterprise users with experience
                (account_tier == "enterprise") &
                (experience >= 5) &
                (dept.is_in("engineering", "design"))
            )
        ) &
        # Level 2: Status and department requirements with nesting
        (
            (
                (user_status == "active") &
                (
                    # Nested department-specific conditions
                    (
                        (dept == "engineering") &
                        (experience >= 2)
                    ) |
                    (
                        (dept == "design") &
                        (user_role.is_in("designer", "lead_designer"))
                    )
                )
            ) |
            # OR admin override
            (user_role == "admin")
        ) &
        # Level 3: First age requirement
        (user_age >= 25) &
        # Level 4: Second age requirement nested with other conditions
        (
            (user_age <= 50) &
            (
                # More nesting for additional validation
                (account_tier.is_in("premium", "enterprise", "trial")) |
                (user_role == "admin")
            )
        )
    ) & (
        # Level 5: Someone later added "additional validation"
        # without realizing it contradicts the previous age requirements!
        (user_age < 25) &  # Must be under 25
        (user_age > 50)    # AND must be over 50 (impossible!)
    ) &
    (available == True)
)

# The contradiction occurs because:
# - Earlier levels require: 25 <= age <= 50
# - Final level requires: age < 25 AND age > 50
# - These conditions cannot both be true!

result, records = optimize(complex_filter)

print(f"Optimized result: {result}")
# Output: FalseExpr()

print(f"Is FALSE: {result is FALSE}")
# Output: True

print(f"Optimization steps that revealed the contradiction:")
for i, record in enumerate(records, 1):
    print(f"Step {i}: {record.reason}")
    if "Contradiction" in record.reason:
        print(f"  *** This step detected the contradiction! ***")

# Example output:
# Step 1: OR equality chain aggregation to IN
# Step 2: Optimize children in AND
# Step 3: Optimize children in OR
# Step 4: Optimize children in AND
# Step 5: Contradiction detected in AND
#   *** This step detected the contradiction! ***

# By examining the 'before' expression in the contradiction record,
# you can identify exactly which requirements conflict with each other
# and trace back through your business logic to find the source.

The optimizer's tracking feature is invaluable for debugging complex business rules, especially when:

  • Multiple developers contribute conditions to the same filter over time
  • Requirements evolve and accidentally introduce conflicts
  • Combining filters from different parts of the application
  • Migrating or refactoring legacy filtering logic
  • Building user-facing query builders where users can create invalid combinations

Optimization Tracking

Track optimization changes:

result, records = optimize(expr)
for record in records:
    print(f"Applied: {record.reason}")
    print(f"Before: {record.before}")
    print(f"After: {record.after}")

You can also use a collecting parameter to accumulate records across multiple optimizations:

my_records = []
result1, _ = optimize(expr1, my_records)
result2, _ = optimize(expr2, my_records)
# my_records now contains all optimization steps from both calls

Expression Evaluation

Expressions can be evaluated against actual data to determine if the data satisfies the filter criteria. This is useful for:

  • In-memory filtering when a database query is not needed
  • Testing and validating filter logic
  • Client-side filtering before sending data to a backend
  • Data validation and access control checks

Basic Evaluation

The eval() method evaluates an expression against a dictionary of field values:

from therismos import F

age = F("age")
status = F("status")

# Build an expression
expr = (age > 18) & (status == "active")

# Evaluate against data
data = {"age": 25, "status": "active"}
result = expr.eval(data)  # Returns True

data = {"age": 15, "status": "active"}
result = expr.eval(data)  # Returns False

Evaluation with Type Casting

When fields have declared types, values are automatically cast during evaluation:

age = F("age", int)
expr = age >= 18

# String values are automatically cast to int
result = expr.eval({"age": "25"})  # Returns True (string "25" cast to int 25)

# This will raise TypeError or ValueError if casting fails
try:
    expr.eval({"age": "not_a_number"})
except (TypeError, ValueError):
    print("Invalid age value")

Evaluating Membership and Regex

All expression types support evaluation:

import re

# IN expressions
status = F("status")
expr = status.is_in("active", "pending", "approved")
result = expr.eval({"status": "active"})  # Returns True

# Regex matching
email = F("email")
expr = email.matches(r".*@example\.com$", re.IGNORECASE)
result = expr.eval({"email": "user@example.com"})  # Returns True

# Null checking
phone = F("phone")
expr = phone.is_null()
result = expr.eval({"phone": None})  # Returns True

Complex Evaluation Examples

Compound expressions evaluate all nested conditions:

age = F("age", int)
country = F("country")
verified = F("verified")
subscription = F("subscription")

# Complex eligibility check
expr = (
    (age >= 18) &
    (country.is_one_of(["US", "UK", "CA"])) &
    ((verified == True) | (subscription.is_in("premium", "enterprise")))
)

# Adult in allowed country with verification
result = expr.eval({
    "age": 25,
    "country": "US",
    "verified": True,
    "subscription": "free"
})  # Returns True

# Adult in allowed country with premium subscription (unverified)
result = expr.eval({
    "age": 30,
    "country": "UK",
    "verified": False,
    "subscription": "premium"
})  # Returns True

# Minor (fails age requirement)
result = expr.eval({
    "age": 16,
    "country": "US",
    "verified": True,
    "subscription": "premium"
})  # Returns False

Evaluation with Optimized Expressions

You can optimize expressions before evaluation for better performance or to catch logical issues:

age = F("age", int)
status = F("status")

# Build a complex expression
expr = (
    ((age > 18) | (age > 25)) &  # Redundant condition
    (status == "active") &
    ((age < 30) | (age >= 30))   # Tautology
)

# Optimize first
optimized, _ = optimize(expr)
# optimized is simplified to: (age > 18) AND TRUE AND (status == "active")
# which further simplifies to: (age > 18) AND (status == "active")

# Then evaluate the optimized expression
result = optimized.eval({"age": 25, "status": "active"})  # Returns True

Error Handling

Evaluation raises exceptions for invalid data:

age = F("age")
expr = age > 18

# Missing field raises KeyError
try:
    expr.eval({"name": "Alice"})  # age field is missing
except KeyError:
    print("Required field 'age' not found")

# Invalid type casting raises TypeError or ValueError
age_typed = F("age", int)
expr = age_typed > 18
try:
    expr.eval({"age": "not_a_number"})
except (TypeError, ValueError):
    print("Cannot cast value to required type")

Converting to other formats

Therismos uses the visitor pattern to enable extensible conversions of expressions to any format. You can implement custom visitors or use the built-in ones.

Custom Visitors

Implement custom visitors to convert expressions to any format:

from therismos import ExprVisitor

class SQLVisitor:
    def visit_eq(self, expr):
        return f"{expr.field.name} = ?"

    def visit_all(self, expr):
        parts = [e.accept(self) for e in expr.exprs]
        return " AND ".join(parts)

    # ... implement other visit methods

visitor = SQLVisitor()
sql = expr.accept(visitor)

Built-in Visitors

Therismos provides several built-in visitors for common use cases:

StringVisitor

Converts expressions to human-readable string representation:

from therismos import F, StringVisitor

age = F("age")
name = F("name")
expr = (age > 18) & (name == "Alice")

visitor = StringVisitor()
result = expr.accept(visitor)
# Output: "(age > 18 AND name = 'Alice')"
CountVisitor

Counts the number of nodes in an expression tree:

from therismos import F, CountVisitor

age = F("age")
name = F("name")
expr = (age > 18) & (name == "Alice")

visitor = CountVisitor()
count = expr.accept(visitor)
# Output: 3 (1 AllExpr + 2 atomic expressions)
DictVisitor

Converts expressions to dictionary representation for serialization:

from therismos import F, DictVisitor

age = F("age")
expr = age > 18

visitor = DictVisitor()
result = expr.accept(visitor)
# Output: {"type": "gt", "field": "age", "value": 18}

For compound expressions, the dictionary is nested:

age = F("age")
name = F("name")
expr = (age > 18) & (name == "Alice")

visitor = DictVisitor()
result = expr.accept(visitor)
# Output: {
#     "type": "and",
#     "exprs": [
#         {"type": "gt", "field": "age", "value": 18},
#         {"type": "eq", "field": "name", "value": "Alice"}
#     ]
# }
FieldGathererVisitor

Collects all unique field names used in an expression tree:

from therismos import F, FieldGathererVisitor

age = F("age")
name = F("name")
status = F("status")
expr = (age > 18) & (name == "Alice") | (status == "active")

visitor = FieldGathererVisitor()
expr.accept(visitor)
field_names = visitor.field_names
# Output: {"age", "name", "status"}

This is useful for:

  • Analyzing which fields are used in complex filters
  • Validating that all referenced fields exist in your schema
  • Generating documentation or metadata about queries
  • Determining required permissions for a query

Backend Converters

MongoVisitor

The MongoVisitor converts therismos expressions to MongoDB query filters compatible with PyMongo and Motor.

Installation:

# For synchronous PyMongo
uv pip install therismos[mongodb]

# For asynchronous Motor
uv pip install therismos[mongodb-async]

Basic Usage:

from therismos import F, optimize
from therismos.expr.visitors.mongo import MongoVisitor

age = F("age")
status = F("status")
country = F("country")

# Build and optimize expression
expr = (age >= 21) & (status == "active") & (country.is_in("US", "UK", "CA"))
optimized, _ = optimize(expr)

# Convert to MongoDB filter
visitor = MongoVisitor()
mongo_filter = optimized.accept(visitor)

# Result: {
#     "age": {"$gte": 21},
#     "status": "active",
#     "country": {"$in": ["US", "UK", "CA"]}
# }

Using with PyMongo:

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017/")
db = client["mydb"]
collection = db["users"]

# Use the generated filter
results = collection.find(mongo_filter)
for doc in results:
    print(doc)

Using with Motor (async):

import asyncio
from motor.motor_asyncio import AsyncIOMotorClient

async def find_users():
    client = AsyncIOMotorClient("mongodb://localhost:27017/")
    db = client["mydb"]
    collection = db["users"]

    # Use the generated filter
    cursor = collection.find(mongo_filter)
    results = await cursor.to_list(length=100)
    return results

asyncio.run(find_users())

Advanced Features:

The MongoVisitor handles all therismos expression types:

import re
from therismos import F, TRUE, FALSE
from therismos.expr.visitors.mongo import MongoVisitor

email = F("email")
age = F("age")
name = F("name")
status = F("status")

visitor = MongoVisitor()

# Regex matching (with case-insensitive flag)
expr = email.matches(r".*@example\.com$", re.IGNORECASE)
mongo_filter = expr.accept(visitor)
# Result: {"email": {"$regex": ".*@example\\.com$", "$options": "i"}}

# Range queries
expr = (age >= 18) & (age <= 65)
mongo_filter = expr.accept(visitor)
# Result: {"age": {"$gte": 18, "$lte": 65}} (optimized)

# Complex OR conditions
expr = (status == "active") | (status == "pending") | (status == "approved")
optimized_expr, _ = optimize(expr)  # Converts to IN
mongo_filter = optimized_expr.accept(visitor)
# Result: {"status": {"$in": ["active", "pending", "approved"]}}

# Null checking
expr = name.is_not_null()
mongo_filter = expr.accept(visitor)
# Result: {"name": {"$ne": null}}

# NOT expressions
expr = ~(age < 18)
mongo_filter = expr.accept(visitor)
# Result: {"$nor": [{"age": {"$lt": 18}}]}

# Constants
true_filter = TRUE.accept(visitor)   # Result: {}
false_filter = FALSE.accept(visitor)  # Result: {"$expr": false}

Optimization Options:

# By default, simple AND expressions are optimized by merging fields
visitor = MongoVisitor(optimize_simple_and=True)
expr = (age > 18) & (name == "Alice")
mongo_filter = expr.accept(visitor)
# Result: {"age": {"$gt": 18}, "name": "Alice"}

# Disable optimization to always use $and
visitor = MongoVisitor(optimize_simple_and=False)
mongo_filter = expr.accept(visitor)
# Result: {"$and": [{"age": {"$gt": 18}}, {"name": "Alice"}]}

Type Casting:

The MongoVisitor respects field type declarations and automatically casts values:

age = F("age", int)
expr = age.is_in(18, 21, 25)  # Values will be cast to int

visitor = MongoVisitor()
mongo_filter = expr.accept(visitor)
# Result: {"age": {"$in": [18, 21, 25]}}

Expression Serialization

Therismos provides grammar-based serialization to convert expressions to/from compact string representations. This is particularly useful for URL query strings, API parameters, and storing filters as text.

Basic Serialization

The Serializer class converts expressions to compact strings without spaces:

from therismos import F, Serializer, Eq, AllExpr, Gt

age = F("age")
score = F("score")

# Create a serializer
serializer = Serializer()

# Serialize an expression
expr = Eq(age, 18)
text = serializer.serialize(expr)
# Result: "age==18"

# Compound expressions
expr = AllExpr(Eq(age, 18), Gt(score, 75))
text = serializer.serialize(expr)
# Result: "(age==18;score>75)"

Deserialization

Convert strings back to expression objects:

# Deserialize a string to an expression
expr = serializer.deserialize("age==18")
# Result: Eq(field=Field(name='age', type_=None), value=18)

# Complex expressions work too
expr = serializer.deserialize('(age>18;status=="active")')
# Result: AllExpr with two conditions

Grammar Operators

The serializer uses a compact grammar optimized for URL usage:

Python Operator Grammar Syntax Example
& (AND) ; age>18;status=="active"
| (OR) , status=="active",status=="pending"
~ (NOT) ! !(age<18)
== == age==18
!= != status!="inactive"
< < age<65
<= <= age<=65
> > age>18
>= >= age>=18
.is_in() =in= status=in=("active","pending")
.matches() ~regex email~regex(".*@example\\.com")
.is_null() ==null deleted_at==null
.is_not_null() !=null created_at!=null
TRUE true() true()
FALSE false() false()

Precedence: ! (NOT) > ; (AND) > , (OR)

URL Encoding

For use in URL query strings, enable URL encoding:

# Create a serializer with URL encoding
serializer = Serializer(url_encode=True)

# Serialize with URL encoding
expr = Eq(F("name"), "Alice Smith")
text = serializer.serialize(expr)
# Result: URL-encoded string

# Deserialize automatically decodes
expr = serializer.deserialize(text)
# Result: Original expression

Type Annotations

Control type annotation output:

age = F("age", int)
name = F("name", str)

# Without type annotations (default)
serializer = Serializer()
text = serializer.serialize(Eq(age, 18))
# Result: "age==18"

# With all type annotations
serializer = Serializer(include_all_types=True)
text = serializer.serialize(Eq(age, 18))
# Result: "age{int}==18"

# Complex example with types
expr = AllExpr(Eq(age, 18), Eq(name, "Alice"))
text = serializer.serialize(expr)
# Result: "(age{int}==18;name{str}==\"Alice\")"

Custom Types

Register custom types for serialization:

def uppercase_transform(x):
    return str(x).upper()

serializer = Serializer()
serializer.register_type(uppercase_transform, "upper")

# Use the custom type
field = F("code", uppercase_transform)
expr = Eq(field, "abc")

# Serialize with type annotation
serializer_typed = Serializer(include_all_types=True)
serializer_typed.register_type(uppercase_transform, "upper")
text = serializer_typed.serialize(expr)
# Result: "code{upper}==\"ABC\"" (value is transformed)

Automatic Value Casting

When deserializing expressions with type annotations, values are automatically cast to the field's declared type:

import uuid
from therismos import Serializer, F

serializer = Serializer()
serializer.register_type(uuid.UUID, 'uuid.UUID')

# Deserialize with type annotation
expr = serializer.deserialize('user_id{uuid.UUID}=="550e8400-e29b-41d4-a716-446655440000"')

# Value is automatically cast to UUID
assert isinstance(expr.value, uuid.UUID)
assert expr.value == uuid.UUID("550e8400-e29b-41d4-a716-446655440000")

# Evaluation works with both string and UUID data
data_uuid = {"user_id": uuid.UUID("550e8400-e29b-41d4-a716-446655440000")}
data_str = {"user_id": "550e8400-e29b-41d4-a716-446655440000"}

assert expr.eval(data_uuid) is True   # Works with UUID
assert expr.eval(data_str) is True    # Works with string (cast automatically)

Automatic casting works for all expression types:

from decimal import Decimal

serializer.register_type(Decimal, 'Decimal')

# Comparison expressions
expr = serializer.deserialize('price{Decimal}>="19.99"')
assert isinstance(expr.value, Decimal)

# IN expressions
expr = serializer.deserialize(
    'status_id{uuid.UUID}=in=("11111111-1111-1111-1111-111111111111",'
    '"22222222-2222-2222-2222-222222222222")'
)
assert all(isinstance(v, uuid.UUID) for v in expr.values)

Implicit Field Types

Define type mappings for field names to avoid repeating type annotations:

import uuid
from decimal import Decimal
from therismos import Serializer

# Define field type mappings
field_types = {
    "user_id": uuid.UUID,
    "product_id": uuid.UUID,
    "order_id": uuid.UUID,
    "price": Decimal,
    "amount": Decimal,
}

serializer = Serializer(field_types=field_types)
serializer.register_type(uuid.UUID, 'uuid.UUID')
serializer.register_type(Decimal, 'Decimal')

# No type annotation needed - uses implicit mapping
expr = serializer.deserialize('user_id=="550e8400-e29b-41d4-a716-446655440000"')
assert expr.field.type_ is uuid.UUID
assert isinstance(expr.value, uuid.UUID)

# Works in compound expressions
expr = serializer.deserialize(
    'user_id=="550e8400-e29b-41d4-a716-446655440000";price>="19.99"'
)
# Both fields automatically get their types

You can also register field types programmatically:

serializer = Serializer()
serializer.register_type(uuid.UUID, 'uuid.UUID')
serializer.register_field_type("account_id", uuid.UUID)

expr = serializer.deserialize('account_id=="11111111-1111-1111-1111-111111111111"')
assert expr.field.type_ is uuid.UUID

Explicit type annotations always override implicit mappings:

field_types = {"amount": Decimal}
serializer = Serializer(field_types=field_types)

# Explicit type overrides implicit
expr = serializer.deserialize('amount{int}=="100"')
assert expr.field.type_ is int  # Not Decimal
assert expr.value == 100

Benefits of Implicit Field Types:

  • Cleaner serialization: No need to annotate every field
  • Centralized schema: Define types once, use everywhere
  • Type safety: Automatic casting during deserialization
  • Flexibility: Explicit annotations still work and override implicit types

Complete Example

from therismos import F, Serializer, optimize

# Build a complex expression
age = F("age", int)
status = F("status")
country = F("country")

expr = (
    (age >= 21) &
    ((status == "active") | (status == "pending")) &
    (country.is_in("US", "UK", "CA"))
)

# Optimize it
optimized, _ = optimize(expr)
# The OR chain becomes an IN expression

# Serialize for a URL query parameter
serializer = Serializer(url_encode=True)
query_param = serializer.serialize(optimized)
# Use in URL: /api/users?filter={query_param}

# Later, deserialize from the URL parameter
received_expr = serializer.deserialize(query_param)
# Use the expression for database queries, validation, etc.

Value Types

The serializer handles various value types:

serializer = Serializer()

# Strings (double-quoted with escapes)
expr = Eq(F("name"), "Alice")
# Result: "name==\"Alice\""

# Numbers (integers and floats)
expr = Eq(F("age"), 25)
# Result: "age==25"
expr = Eq(F("price"), 19.99)
# Result: "price==19.99"

# Booleans
expr = Eq(F("active"), True)
# Result: "active==true"

# Null
expr = Eq(F("value"), None)
# Result: "value==null"

# Identifiers (unquoted - interpreted as strings)
expr = serializer.deserialize("status==active")
# value is the string "active"

Field Names with Dots

The serializer supports nested field references using dot notation:

expr = Eq(F("user.profile.age"), 25)
text = serializer.serialize(expr)
# Result: "user.profile.age==25"

Module Structure

Therismos is organized into the following modules and submodules:

therismos/
├── __init__.py              # Main package exports
└── expr/                    # Expression module
    ├── __init__.py          # Expression module exports
    ├── _expr.py             # Core expression classes (Expr, Field, operators, etc.)
    ├── optimizer.py         # Expression optimization and simplification
    ├── serializer.py        # Grammar-based string serialization/deserialization
    └── visitors/            # Visitor implementations package
        ├── __init__.py      # Core visitor exports
        ├── _visitors.py     # Built-in visitor implementations
        └── mongo.py         # MongoDB query filter converter

Core Modules

  • therismos.expr: Core expression AST implementation

    • Expression types: Eq, Ne, Lt, Le, Gt, Ge, Regex, In, IsNull
    • Compound expressions: AllExpr, AnyExpr, NotExpr
    • Logical constants: TRUE, FALSE
    • Field types: Field, F (helper function)
    • Visitor protocol: ExprVisitor
    • Serialization: Serializer (grammar-based string conversion)
  • therismos.expr.optimizer: Expression optimization

    • optimize(expr, records=None): Optimize an expression tree
    • OptimizationRecord: Records of optimization transformations
  • therismos.expr.serializer: Grammar-based serialization

    • Serializer: Converts expressions to/from compact string representations
    • URL encoding support for query parameters
    • Type annotation control
    • Custom type registration
  • therismos.expr.visitors: Built-in visitor implementations

    • StringVisitor: Converts expressions to human-readable strings
    • CountVisitor: Counts nodes in expression trees
    • DictVisitor: Converts expressions to dictionary representation
    • FieldGathererVisitor: Collects all field names used in an expression
  • therismos.expr.visitors.mongo: MongoDB backend converter

    • MongoVisitor: Converts expressions to MongoDB query filters for PyMongo/Motor

Development

Requires Python 3.11 or higher.

Setup

# Install dependencies
uv pip install -e ".[dev]"

# Run tests
pytest

# Run linting
ruff check therismos tests

# Run type checking
mypy therismos

# Run all checks with tox
tox

Testing

The project uses pytest with extensive parametrization for comprehensive test coverage:

# Run all tests
pytest

# Run with coverage
pytest --cov=therismos --cov-report=html

# Run specific test file
pytest tests/test_optimizer.py

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

therismos-0.2.0.tar.gz (50.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

therismos-0.2.0-py3-none-any.whl (31.1 kB view details)

Uploaded Python 3

File details

Details for the file therismos-0.2.0.tar.gz.

File metadata

  • Download URL: therismos-0.2.0.tar.gz
  • Upload date:
  • Size: 50.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.7

File hashes

Hashes for therismos-0.2.0.tar.gz
Algorithm Hash digest
SHA256 a50196423045505f979460d6163764878d7f783658c0c3667621196bd2adb456
MD5 d614ce03e305591e09b82f24a00e7bcd
BLAKE2b-256 b473c4dfc7e53e88606e2e386e839d3da3948b8d66e35f6204a93fa7bdfa3364

See more details on using hashes here.

File details

Details for the file therismos-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: therismos-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 31.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.7

File hashes

Hashes for therismos-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 265fd54d37afd08af619f02bf616def1c784d7828863e0b276d96d5f9169e055
MD5 f059c1fe138bda70379c10efe800ebd3
BLAKE2b-256 223d0699967e1f5fb81a3b38dc4421368bd103a963376dad314a01f1872223a9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page