Name | boolia JSON |
Version |
0.1.2
JSON |
| download |
home_page | None |
Summary | Tiny, safe boolean-expression language with dotted paths, functions, and rule books. |
upload_time | 2025-10-13 03:07:07 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.8 |
license | MIT |
keywords |
boolean
dsl
expression
jinja-like
logic
rules
|
VCS |
 |
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# boolia
A tiny, safe **boolean expression** engine: like Jinja for logic.
- **Grammar**: `and`, `or`, `not`, parentheses, comparisons (`== != > >= <= <`), `in`
- **Values**: numbers, strings, booleans, `null/None`, identifiers, dotted paths (`user.age`, `house.light.on`)
- **Tags**: bare identifiers evaluate `True` if present in a `tags: set[str]`
- **Functions**: user-registered, safe callables (`starts_with`, `matches`, ...)
- **RuleBook**: name your rules and evaluate them later
- **RuleGroup**: compose rules with `all`/`any` semantics and nested groups
- **Missing policy**: choose to **raise** or substitute **None/False/custom default**
```py
from boolia import evaluate, RuleBook, DEFAULT_FUNCTIONS
expr = "(car and elephant) or house.light.on"
print(evaluate(expr, context={"house": {"light": {"on": True}}}, tags={"car"})) # True
```
## Install
```bash
pip install boolia
```
## Tooling
The project ships with Ruff for linting and MyPy for type checking. After installing the
development extras you can run the primary checks with:
```bash
ruff check .
mypy .
```
## Quick start
```py
from boolia import evaluate, DEFAULT_FUNCTIONS
ctx = {"user": {"age": 21, "roles": ["admin", "ops"]}}
tags = {"beta"}
expr = "user.age >= 18 and 'admin' in user.roles"
print(evaluate(expr, context=ctx, tags=tags)) # True
```
### Functions
```py
from boolia import evaluate, DEFAULT_FUNCTIONS
DEFAULT_FUNCTIONS.register("starts_with", lambda s, p: str(s).startswith(str(p)))
expr = "starts_with(user.name, 'Sn')"
print(evaluate(expr, context={"user": {"name": "Snoopy"}})) # True
```
### Custom operators
```py
from boolia import evaluate, DEFAULT_OPERATORS
custom_ops = DEFAULT_OPERATORS.copy()
custom_ops.register(
"XOR", # The operator identifier
precedence=20, # Higher precedence than AND/OR
evaluator=lambda left, right: bool(left) ^ bool(right), # XOR logic
keywords=("xor",), # Use "xor" in expressions
)
print(evaluate("true xor false", operators=custom_ops)) # True
print(evaluate("true xor true", operators=custom_ops)) # False
```
Operators can be declared with `keywords=("xor",)` for word-style syntax or `symbols=("^",)`
for symbolic tokens. Use `compile_rule(expr, operators=custom_ops)` to persist custom
operators inside compiled rules. When evaluating rules or rule groups you can still pass a
different registry with `operators=` if you need to override their behavior.
### RuleBook
```py
from boolia import RuleBook, RuleGroup
rules = RuleBook()
rules.add("adult", "user.age >= 18")
rules.add("brazilian", "starts_with(user.country, 'Br')")
rules.add("vip", "contains(user.roles, 'vip')")
rules.add_group(
"eligible",
mode="all",
members=[
"adult",
RuleGroup(mode="any", members=["brazilian", "vip"]),
],
)
ok = rules.evaluate(
"eligible",
context={"user": {"age": 22, "country": "Brazil", "roles": ["member"]}},
)
print(ok) # True
print(rules.evaluate("eligible", context={"user": {"age": 22, "country": "Chile", "roles": ["vip"]}})) # True
print(rules.evaluate("eligible", context={"user": {"age": 17, "country": "Chile", "roles": ["member"]}})) # False
```
`RuleGroup` members can be rule names, already compiled `Rule` objects, or other `RuleGroup` instances. Nested groups short-circuit according to their mode (`all`/`any`), empty groups are vacuously `True`/`False`, and cycles raise a helpful error. Add groups with `RuleBook.add_group` or register existing ones with `RuleBook.register`.
### Missing policy
```py
from boolia import evaluate, MissingVariableError
try:
evaluate("user.age >= 18 and house.light.on", context={"user": {"age": 20}}, on_missing="raise")
except MissingVariableError as e:
print(e) # Missing variable/path: house.light.on
print(evaluate("score >= 10", context={}, on_missing="default", default_value=0)) # False
print(evaluate("flag and beta", context={}, tags={"beta"}, on_missing="none")) # False (flag is None)
```
### Notes
- Use `on_missing="none"` if you want **tags to override** missing bare identifiers.
- For stricter semantics on dotted paths, keep `on_missing="raise"` and allow tags only for bare names.
## Local development
```bash
pip install -e .[dev]
pytest -q
ruff check .
mypy .
```
Raw data
{
"_id": null,
"home_page": null,
"name": "boolia",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": "Jo\u00e3o Freires <joaofreires@tutanota.com>",
"keywords": "boolean, dsl, expression, jinja-like, logic, rules",
"author": null,
"author_email": "Jo\u00e3o Freires <joaofreires@tutanota.com>",
"download_url": "https://files.pythonhosted.org/packages/97/2a/9566fb93c6fd7236068df99335cdfc467289eb3724f650b8d929efb2a655/boolia-0.1.2.tar.gz",
"platform": null,
"description": "# boolia\n\nA tiny, safe **boolean expression** engine: like Jinja for logic.\n\n- **Grammar**: `and`, `or`, `not`, parentheses, comparisons (`== != > >= <= <`), `in`\n- **Values**: numbers, strings, booleans, `null/None`, identifiers, dotted paths (`user.age`, `house.light.on`)\n- **Tags**: bare identifiers evaluate `True` if present in a `tags: set[str]`\n- **Functions**: user-registered, safe callables (`starts_with`, `matches`, ...)\n- **RuleBook**: name your rules and evaluate them later\n- **RuleGroup**: compose rules with `all`/`any` semantics and nested groups\n- **Missing policy**: choose to **raise** or substitute **None/False/custom default**\n\n```py\nfrom boolia import evaluate, RuleBook, DEFAULT_FUNCTIONS\n\nexpr = \"(car and elephant) or house.light.on\"\nprint(evaluate(expr, context={\"house\": {\"light\": {\"on\": True}}}, tags={\"car\"})) # True\n```\n\n## Install\n\n```bash\npip install boolia\n```\n\n## Tooling\n\nThe project ships with Ruff for linting and MyPy for type checking. After installing the\ndevelopment extras you can run the primary checks with:\n\n```bash\nruff check .\nmypy .\n```\n\n## Quick start\n\n```py\nfrom boolia import evaluate, DEFAULT_FUNCTIONS\n\nctx = {\"user\": {\"age\": 21, \"roles\": [\"admin\", \"ops\"]}}\ntags = {\"beta\"}\nexpr = \"user.age >= 18 and 'admin' in user.roles\"\nprint(evaluate(expr, context=ctx, tags=tags)) # True\n```\n\n### Functions\n\n```py\nfrom boolia import evaluate, DEFAULT_FUNCTIONS\n\nDEFAULT_FUNCTIONS.register(\"starts_with\", lambda s, p: str(s).startswith(str(p)))\n\nexpr = \"starts_with(user.name, 'Sn')\"\nprint(evaluate(expr, context={\"user\": {\"name\": \"Snoopy\"}})) # True\n```\n\n### Custom operators\n\n```py\nfrom boolia import evaluate, DEFAULT_OPERATORS\n\ncustom_ops = DEFAULT_OPERATORS.copy()\ncustom_ops.register(\n \"XOR\", # The operator identifier\n precedence=20, # Higher precedence than AND/OR\n evaluator=lambda left, right: bool(left) ^ bool(right), # XOR logic\n keywords=(\"xor\",), # Use \"xor\" in expressions\n)\n\nprint(evaluate(\"true xor false\", operators=custom_ops)) # True\nprint(evaluate(\"true xor true\", operators=custom_ops)) # False\n```\n\nOperators can be declared with `keywords=(\"xor\",)` for word-style syntax or `symbols=(\"^\",)`\nfor symbolic tokens. Use `compile_rule(expr, operators=custom_ops)` to persist custom\noperators inside compiled rules. When evaluating rules or rule groups you can still pass a\ndifferent registry with `operators=` if you need to override their behavior.\n\n### RuleBook\n\n```py\nfrom boolia import RuleBook, RuleGroup\n\nrules = RuleBook()\nrules.add(\"adult\", \"user.age >= 18\")\nrules.add(\"brazilian\", \"starts_with(user.country, 'Br')\")\nrules.add(\"vip\", \"contains(user.roles, 'vip')\")\nrules.add_group(\n \"eligible\",\n mode=\"all\",\n members=[\n \"adult\",\n RuleGroup(mode=\"any\", members=[\"brazilian\", \"vip\"]),\n ],\n)\n\nok = rules.evaluate(\n \"eligible\",\n context={\"user\": {\"age\": 22, \"country\": \"Brazil\", \"roles\": [\"member\"]}},\n)\nprint(ok) # True\n\nprint(rules.evaluate(\"eligible\", context={\"user\": {\"age\": 22, \"country\": \"Chile\", \"roles\": [\"vip\"]}})) # True\nprint(rules.evaluate(\"eligible\", context={\"user\": {\"age\": 17, \"country\": \"Chile\", \"roles\": [\"member\"]}})) # False\n```\n\n`RuleGroup` members can be rule names, already compiled `Rule` objects, or other `RuleGroup` instances. Nested groups short-circuit according to their mode (`all`/`any`), empty groups are vacuously `True`/`False`, and cycles raise a helpful error. Add groups with `RuleBook.add_group` or register existing ones with `RuleBook.register`.\n\n### Missing policy\n\n```py\nfrom boolia import evaluate, MissingVariableError\n\ntry:\n evaluate(\"user.age >= 18 and house.light.on\", context={\"user\": {\"age\": 20}}, on_missing=\"raise\")\nexcept MissingVariableError as e:\n print(e) # Missing variable/path: house.light.on\n\nprint(evaluate(\"score >= 10\", context={}, on_missing=\"default\", default_value=0)) # False\nprint(evaluate(\"flag and beta\", context={}, tags={\"beta\"}, on_missing=\"none\")) # False (flag is None)\n```\n\n### Notes\n\n- Use `on_missing=\"none\"` if you want **tags to override** missing bare identifiers.\n- For stricter semantics on dotted paths, keep `on_missing=\"raise\"` and allow tags only for bare names.\n\n## Local development\n\n```bash\npip install -e .[dev]\npytest -q\nruff check .\nmypy .\n```\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Tiny, safe boolean-expression language with dotted paths, functions, and rule books.",
"version": "0.1.2",
"project_urls": {
"Changelog": "https://github.com/joaofreires/boolia/releases",
"Documentation": "https://github.com/joaofreires/boolia#readme",
"Homepage": "https://github.com/joaofreires/boolia",
"Issues": "https://github.com/joaofreires/boolia/issues",
"Repository": "https://github.com/joaofreires/boolia"
},
"split_keywords": [
"boolean",
" dsl",
" expression",
" jinja-like",
" logic",
" rules"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "a254edbf3d46fe47da6f0cc9e323e2b9c91aa36a59fca035eb0cb18e7851a1a3",
"md5": "0f44ff0a18e1245bc162a781a482ae06",
"sha256": "5e20ec9fdcc12bf548310caf59833ae61ea5ef2be8164d6e0ed8c9594ef1f74a"
},
"downloads": -1,
"filename": "boolia-0.1.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "0f44ff0a18e1245bc162a781a482ae06",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 11637,
"upload_time": "2025-10-13T03:07:06",
"upload_time_iso_8601": "2025-10-13T03:07:06.724254Z",
"url": "https://files.pythonhosted.org/packages/a2/54/edbf3d46fe47da6f0cc9e323e2b9c91aa36a59fca035eb0cb18e7851a1a3/boolia-0.1.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "972a9566fb93c6fd7236068df99335cdfc467289eb3724f650b8d929efb2a655",
"md5": "d7169dccdc56bc761dadb170390f83af",
"sha256": "79a92e2e884bd772274c45ce9f28f72cc71422282dc8bec87c79fd7aa960eda3"
},
"downloads": -1,
"filename": "boolia-0.1.2.tar.gz",
"has_sig": false,
"md5_digest": "d7169dccdc56bc761dadb170390f83af",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 30305,
"upload_time": "2025-10-13T03:07:07",
"upload_time_iso_8601": "2025-10-13T03:07:07.816298Z",
"url": "https://files.pythonhosted.org/packages/97/2a/9566fb93c6fd7236068df99335cdfc467289eb3724f650b8d929efb2a655/boolia-0.1.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-10-13 03:07:07",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "joaofreires",
"github_project": "boolia",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "boolia"
}