Source code for pyvergeos.filters

"""OData-style filter expression builder for VergeOS API queries."""

from enum import Enum
from typing import Any, Union


[docs] class FilterOperator(Enum): """Supported filter operators.""" EQ = "eq" NE = "ne" LT = "lt" GT = "gt" LE = "le" GE = "ge" LIKE = "like" IN = "in"
[docs] class Filter: """OData-style filter expression builder. Example: >>> f = Filter() >>> f.eq("status", "running").and_().like("name", "web*") >>> str(f) "status eq 'running' and name like 'web%'" """
[docs] def __init__(self) -> None: self._parts: list[str] = []
def _add(self, field: str, op: FilterOperator, value: Any) -> "Filter": """Add a filter condition.""" formatted_value = self._format_value(value, op) self._parts.append(f"{field} {op.value} {formatted_value}") return self def _format_value(self, value: Any, op: FilterOperator) -> str: """Format value for filter expression.""" if op == FilterOperator.IN: if not isinstance(value, (list, tuple)): value = [value] formatted = ", ".join(self._format_single(v) for v in value) return f"({formatted})" if op == FilterOperator.LIKE and isinstance(value, str): # Convert wildcards: * -> %, ? -> _ value = value.replace("*", "%").replace("?", "_") return self._format_single(value) def _format_single(self, value: Any) -> str: """Format a single value.""" if value is None: return "null" if isinstance(value, bool): return "true" if value else "false" if isinstance(value, (int, float)): return str(value) # String - quote and escape value = str(value).replace("'", "''") return f"'{value}'" def _auto_and(self) -> None: """Auto-add AND if needed (implicit AND between conditions).""" if self._parts and self._parts[-1] not in ("and", "or"): self._parts.append("and")
[docs] def eq(self, field: str, value: Any) -> "Filter": """Add equals condition.""" self._auto_and() return self._add(field, FilterOperator.EQ, value)
[docs] def ne(self, field: str, value: Any) -> "Filter": """Add not equals condition.""" self._auto_and() return self._add(field, FilterOperator.NE, value)
[docs] def lt(self, field: str, value: Any) -> "Filter": """Add less than condition.""" self._auto_and() return self._add(field, FilterOperator.LT, value)
[docs] def gt(self, field: str, value: Any) -> "Filter": """Add greater than condition.""" self._auto_and() return self._add(field, FilterOperator.GT, value)
[docs] def le(self, field: str, value: Any) -> "Filter": """Add less than or equal condition.""" self._auto_and() return self._add(field, FilterOperator.LE, value)
[docs] def ge(self, field: str, value: Any) -> "Filter": """Add greater than or equal condition.""" self._auto_and() return self._add(field, FilterOperator.GE, value)
[docs] def like(self, field: str, pattern: str) -> "Filter": """Add LIKE pattern condition. Use * for wildcard.""" self._auto_and() return self._add(field, FilterOperator.LIKE, pattern)
[docs] def in_(self, field: str, values: Union[list[Any], Any]) -> "Filter": """Add IN condition.""" self._auto_and() return self._add(field, FilterOperator.IN, values)
[docs] def and_(self) -> "Filter": """Add explicit AND connector (usually not needed, AND is implicit).""" self._parts.append("and") return self
[docs] def or_(self) -> "Filter": """Add OR connector (must be explicit, unlike AND).""" self._parts.append("or") return self
def __str__(self) -> str: return " ".join(self._parts) def __bool__(self) -> bool: return bool(self._parts) def __repr__(self) -> str: return f"Filter({str(self)!r})"
def _format_value(value: Any) -> str: """Format a value for a filter expression.""" if value is None: return "null" if isinstance(value, bool): return "true" if value else "false" if isinstance(value, (int, float)): return str(value) value = str(value).replace("'", "''") return f"'{value}'"
[docs] def build_filter(**kwargs: Any) -> str: """Build a filter string from keyword arguments. Supports: - Simple equality: name="value" - Wildcards: name="prefix*" (converted to LIKE) - Lists: status=["running", "stopped"] (converted to IN) Args: **kwargs: Field-value pairs for filtering. Returns: OData filter string. Example: >>> build_filter(status="running", name="web*") "status eq 'running' and name like 'web%'" """ parts = [] for field, value in kwargs.items(): if value is None: continue if isinstance(value, (list, tuple)): # IN query formatted = ", ".join(_format_value(v) for v in value) parts.append(f"{field} in ({formatted})") elif isinstance(value, str) and ("*" in value or "?" in value): # LIKE query pattern = value.replace("*", "%").replace("?", "_") parts.append(f"{field} like '{pattern}'") else: # Equality parts.append(f"{field} eq {_format_value(value)}") return " and ".join(parts)