Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

A Python package for parsing boolean conditional expressions from strings using pyparsing. Converts string expressions like "x > 1 and y < 2" into object representations and SQLAlchemy filter conditions.

**Core Components:**
- **Parser**: Base parser for boolean conditional expressions
- **SQLAParser**: Specialized parser that converts strings to SQLAlchemy filter clauses
- **Clauses**: pyparsing grammar definitions for conditionals, operators, and boolean logic
- **Actions**: Classes that control parsing behavior for each clause type
- **Mixins**: SQLAMixin provides SQLAlchemy filter conversion capabilities

## Development Commands

### Testing
```bash
# Run all tests with coverage
pytest

# Run tests with coverage report
pytest --cov boolean_parser --cov-report html

# Run specific test file
pytest tests/parsers/test_parser.py

# Run specific test
pytest tests/parsers/test_parser.py::test_name -v
```

### Code Quality
```bash
# Lint with flake8
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=99 --statistics

# Sort imports (isort config in setup.cfg)
isort boolean_parser/
```

### Documentation
```bash
# Build Sphinx docs
invoke docs.build

# Build docs with clean
invoke docs.build --clean

# Show built docs in browser
invoke docs.show

# Clean docs build directory
invoke docs.clean
```

### Package Management
```bash
# Install in development mode
pip install -e .[dev]

# Install with docs extras
pip install -e .[docs]

# Clean build artifacts
invoke clean

# Deploy to PyPI (after invoke clean)
invoke deploy
```

## Architecture

### Parsing Flow
1. **Input String** → `Parser.parse()` or `SQLAParser.parse()`
2. **pyparsing Grammar** → Clauses define pattern matching (condition, between_cond, words)
3. **Parse Actions** → Actions convert parsed tokens to objects (Condition, Word)
4. **Boolean Logic** → infixNotation handles NOT→AND→OR precedence using BoolNot, BoolAnd, BoolOr
5. **Output Object** → Returns structured representation with `.params` and `.conditions` attributes

### Parser Class Hierarchy
```
Parser (base class - boolean_parser/parsers/base.py)
├─ Clauses: condition, between_cond, words
├─ Actions: Condition, Word
└─ Booleans: BoolNot, BoolAnd, BoolOr

SQLAParser (extends Parser - boolean_parser/parsers/sqla.py)
├─ Clauses: condition, between_cond (no words clause)
├─ Actions: SQLACondition (uses SQLAMixin)
└─ Booleans: SQLANot, SQLAAnd, SQLAOr (with .filter() method)
```

### SQLAlchemy Integration Pattern
The SQLAParser uses a mixin pattern to add SQLAlchemy capabilities:
- **SQLAMixin** (boolean_parser/mixins/sqla.py): Provides `filter()` method for converting parsed conditions to SQLAlchemy filter expressions
- **SQLACondition** (inherits from both Condition and SQLAMixin): Parses "table.column operator value" syntax
- **SQLBoolBase**: Boolean classes override to call `filter()` on nested conditions with sqlalchemy.sql operators (and_, or_, not_)

### Clause Definitions (boolean_parser/clauses.py)
- **condition**: `name operator value` (e.g., "x > 5", "table.column = 'test'")
- **between_cond**: `name between value1 and value2` (e.g., "age between 18 and 65")
- **words**: Simple word tokens (used by base Parser only)
- **Operators**: ==, <=, <, >, >=, =, !=, &, | plus special handling for wildcards (*) and NULL

### Parser Customization
Use `Parser.build_parser(clauses, actions, bools)` to create custom parsers with:
- Custom clause definitions (pyparsing elements)
- Custom actions (classes/functions for parse behavior)
- Custom boolean logic handlers (must be [NotClass, AndClass, OrClass])

## Key Implementation Details

### String Condition Syntax
- **Base**: `parameter operator value` → "x > 1", "name = 'test*'" (wildcards supported)
- **SQLAlchemy**: `table.parameter operator value` → "users.age > 18", "users.name = '*smith'"
- **Between**: `parameter between value1 and value2` → "age between 18 and 65"
- **Boolean Logic**: Join with "and", "or", "not" → "x > 1 and y < 2 or not z = 3"

### SQLAlchemy Filter Conversion
When calling `result.filter(ModelClass)`:
1. Matches parsed parameter names to ModelClass attributes via tablename
2. Handles type casting (int, float, bool, date, datetime) based on column type
3. Converts operators: `=` becomes LIKE with wildcards, `==` is exact equality
4. Special handling: NULL values, PostgreSQL arrays, bitwise operations
5. Uses bindparam for safe parameter binding

## Testing Strategy

Tests use pytest with fixtures in tests/conftest.py:
- **database.py**: In-memory SQLite database setup
- **models.py**: SQLAlchemy test models (Table, Thing, etc.)
- **factory_boy**: Factories for test data generation via pytest-factoryboy
- **Test structure**: tests/parsers/ for parser tests, tests/test_*.py for unit tests

## Configuration Files

- **setup.cfg**: Package metadata, dependencies, tool configs (isort, flake8, pytest, coverage)
- **tasks.py**: Invoke tasks for docs and deployment
- **.github/workflows/**: CI runs tests, linting, and Sphinx doc builds on push/PR

## Version & Dependencies

- **Python**: >= 3.6
- **Required**: six >= 1.16.0, pyparsing >= 2.4, sqlalchemy >= 1.4.0
- **Dev**: pytest, pytest-cov, factory_boy, pytest-factoryboy, ipython
- **Docs**: Sphinx, sphinx_bootstrap_theme, sphinx-issues

## Common Patterns

### Creating a Custom Parser
```python
from boolean_parser.parsers import Parser
from boolean_parser.clauses import condition, words
from boolean_parser.actions.clause import Condition, Word

clauses = [condition, words]
actions = [Condition, Word]
Parser.build_parser(clauses=clauses, actions=actions)
```

### Using the Convenience Function
```python
from boolean_parser import parse

# Use base parser
result = parse('x > 1 and y < 2', base='base')

# Use SQLAlchemy parser (default)
result = parse('table.x > 5', base='sqla')
```

### SQLAlchemy Query Pattern
```python
from boolean_parser.parsers import SQLAParser
from models import TableModel
from database import session

# Parse string condition
parser = SQLAParser('table.x > 5 and table.y < 2')
result = parser.parse()

# Convert to SQLAlchemy filter
filter_condition = result.filter(TableModel)

# Use in query
query = session.query(TableModel).filter(filter_condition).all()
```
Loading