An open API service indexing awesome lists of open source software.

https://github.com/ertgl/django-xformula

Django query evaluator, built on top of XFormula language front-end.
https://github.com/ertgl/django-xformula

django domain-specific-language dynamic-filtering python query

Last synced: 8 months ago
JSON representation

Django query evaluator, built on top of XFormula language front-end.

Awesome Lists containing this project

README

          

# django-xformula

A dynamic formula-to-Python and formula-to-SQL evaluator for
[Django](https://www.djangoproject.com/) applications.

## Table of Contents

- [Overview](#overview)
- [Use Cases](#use-cases)
- [Features](#features)
- [Bidirectional Operators](#bidirectional-operators)
- [Zero Built-in Variables by Default](#zero-built-in-variables-by-default)
- [Customizable Attribute Getter](#customizable-attribute-getter)
- [Customizable Function Caller](#customizable-function-caller)
- [Installation](#installation)
- [Usage](#usage)
- [Operators](#operators)
- [Syntax](#syntax)
- [Troubleshooting](#troubleshooting)
- [License](#license)

## Overview

django-xformula is a flexible query evaluator built for Django applications. It
uses the default syntax grammar and parser generated by the
[XFormula](https://github.com/ertgl/xformula) project, to translate formulas
into Django ORM query expressions. With its dynamic evaluation engine, it
becomes very easy to design a DSL (domain-specific language) for defining,
storing, and loading complex queries, opening the door to a wide range of
advanced use cases.

### Use Cases

Here are some typical ways django-xformula can be used:

- **Open APIs**: Let external clients or integrations filter, annotate, or
aggregate data with combinable formulas.
- **Authorization Rules**: Store and enforce query-based authorization rules
directly in the database.
- **Dynamic Business Rules**: Update business rules on the fly without
redeploying, giving departments control over their own rules.
- **Personalized Experiences**: Allow end users to adjust how they interact
with your app, such as pre-filtering, sorting, or pre-defining conditions for
webhooks.

Of course, these are just examples. Any situation where you need dynamic query
evaluation can benefit.

### Features

django-xformula focuses on being minimal but powerful, giving you the building
blocks to create query evaluators suited to your needs.

#### Bidirectional Operators

The same syntax works for both Python and Django query evaluations. Expressions
are interpreted based on context:

- With a `QuerySet`, evaluation returns a `QuerySet`.
- With a `Q` object, evaluation produces a `Q`.
- With a `Combinable` or `Field`, it's processed as a `Combinable`.
- With a model instance, evaluation returns a `Value` holding the instance's
primary key.
- Otherwise, expressions fall back to standard Python rules.

This makes it possible to use a formula both as a database query and as a
Python expression. It is particularly useful in scenarios such as filtering
data in GraphQL subscriptions, etc.

#### Zero Built-in Variables by Default

By default, any undefined variable in a formula is treated as a Django `F`
object. This ensures that only approved variables and functions are used in
query evaluations, preventing potential security risks.

#### Customizable Attribute Getter

Direct attribute access is blocked by default. Attempting it raises a
`ForbiddenAttribute` error (a subclass of Django's `PermissionDenied`). You can
customize this to allow safe attributes, adding a strong security layer.

#### Customizable Function Caller

Function calls are also restricted by default. Unauthorized calls raise a
`ForbiddenCall` error (also a subclass of `PermissionDenied`). You can
configure the evaluator to only allow specific functions, balancing flexibility
with safety.

## Installation

django-xformula is available on PyPI and can be installed with pip or any other
compatible package manager:

```sh
pip install django-xformula
```

## Usage

Here's a simple example of using django-xformula to filter a queryset with a
user-supplied formula:

```py
from operator import call

from django.db.models import Q, QuerySet
from django.db.models.functions import Length
from django_xformula import QueryEvaluator

# Import your models.
from myapp.models import MyModel

query_evaluator = QueryEvaluator()

def resource_view(request):
query = request.GET.get("q", "")
context = QueryEvaluator.Context(
# Provide Python objects to the formula context.
builtins={
"Length": Length,
"me": request.user,
},
# WARNING: Allowing arbitrary function calls is unsafe.
# Use a secure function caller (e.g., whitelist checking).
call=call,
# WARNING: Direct attribute access is unsafe.
# Restrict allowed attributes through a secure getter (e.g., whitelist
# checking per object type).
getattr=getattr,
)
q_or_result = query_evaluator.evaluate(q, context)
if isinstance(q_or_result, QuerySet):
return render_table(q_or_result)
if isinstance(q_or_result, Q):
queryset = MyModel.objects.filter(q_or_result)
return render_table(queryset)
# If the formula does not represent a database query,
# return the result of the evaluated expression (e.g., "1 + 1").
return render_result(q_or_result)
```

Sample formulas you can use with the built-ins defined in the example above:

- Records where `owner` is the current user:

```python
owner is me
```

- Records where `owner` is not the current user:

```python
owner is not me
```

- Records where `name` is longer than 5 characters:

```python
Length(name) > 5
```

- Records accessible if the user is staff, the owner, or the record is public:

```python
me.is_staff or me is owner or is_public
```

- Records where `version` is the current version, but only if a condition
passes first:

```python
check_condition() and version is CURRENT_VERSION
```

### Operators

For a full list of supported operators, see the
[XFormula default precedence list](https://github.com/ertgl/xformula/blob/main/src/xformula/syntax/core/operations/default_operator_precedences.py#L16).

### Syntax

The XFormula project provides a default syntax, where the top-level
non-terminal type is `expression`. django-xformula uses this syntax by default.
If customization is needed, you can find the relevant examples in the XFormula
[README](https://github.com/ertgl/xformula) and in its
[features](https://github.com/ertgl/xformula/tree/4e1e3a88200ccbe22d8aa477001d9af68dd91ab4/src/xformula/syntax/core/features).

You can also check the EBNF grammar of the default syntax in the
[out/Grammar.lark](https://github.com/ertgl/xformula/blob/main/out/Grammar.lark)
file.

### Troubleshooting

Some common issues you might run into:

- **`ForbiddenAttribute` error**: The formula is trying to access an attribute
that isn't allowed. Update your custom attribute getter to allow it.
- **`ForbiddenCall` error**: The formula is trying to call a function that
isn't allowed. Update your custom function caller to whitelist the function.
- **Invalid query syntax**: The formula doesn't match XFormula grammar.
Double-check the syntax.
- **Unsupported database function**: The database backend doesn't support the
function you're trying to use. Verify
[backend capabilities](https://docs.djangoproject.com/en/5.2/ref/databases/).

## License

This project is licensed under the
[MIT License](https://opensource.org/license/mit).

See the [LICENSE](LICENSE) file for details.