https://github.com/daireto/odata-v4-query
A lightweight, simple and fast parser for OData V4 query options supporting standard query parameters. Provides helper functions to apply OData V4 query options to ORM/ODM queries such as SQLAlchemy and Beanie.
https://github.com/daireto/odata-v4-query
beanie-odm odata odata-query-parser odatav4 pymongo sqlalchemy tokenizer-parser
Last synced: 29 days ago
JSON representation
A lightweight, simple and fast parser for OData V4 query options supporting standard query parameters. Provides helper functions to apply OData V4 query options to ORM/ODM queries such as SQLAlchemy and Beanie.
- Host: GitHub
- URL: https://github.com/daireto/odata-v4-query
- Owner: daireto
- License: mit
- Created: 2025-03-03T03:31:14.000Z (12 months ago)
- Default Branch: main
- Last Pushed: 2025-11-09T04:59:55.000Z (3 months ago)
- Last Synced: 2025-11-09T06:17:23.452Z (3 months ago)
- Topics: beanie-odm, odata, odata-query-parser, odatav4, pymongo, sqlalchemy, tokenizer-parser
- Language: Python
- Homepage: https://pypi.org/project/odata-v4-query/
- Size: 202 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE.md
- Security: SECURITY.md
Awesome Lists containing this project
README
# OData V4 Query
A lightweight, simple and fast parser for OData V4 query options supporting
standard query parameters. Provides helper functions to apply OData V4 query
options to ORM/ODM queries such as SQLAlchemy, PyMongo and Beanie.
## Table of Contents
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Utility Functions](#utility-functions)
- [Beanie](#beanie)
- [PyMongo](#pymongo)
- [SQLAlchemy](#sqlalchemy)
- [Contributing](#contributing)
- [License](#license)
- [Support](#support)
## Features
- Support for the following OData V4 standard query parameters:
- `$count` - Include count of items
- `$expand` - Expand related entities
- `$filter` - Filter results
- `$format` - Response format (json, xml, csv, tsv)
- `$orderby` - Sort results
- `$search` - Search items
- `$select` - Select specific fields
- `$skip` - Skip N items
- `$top` - Limit to N items
- `$page` - Page number
- Comprehensive filter expression support:
- Comparison operators: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `in`, `nin`
- Logical operators: `and`, `or`, `not`, `nor`
- Collection operators: `has`
- String functions: `startswith`, `endswith`, `contains`, `substring`, `tolower`, `toupper`
- Nested field filtering: `user/name`, `profile/address/city`
- Utility functions to apply options to ORM/ODM queries.
- See [utility functions](#utility-functions) for more information.
## Requirements
- `Python 3.10+`
- `beanie 1.23+ (optional, for Beanie ODM utils)`
- `pymongo 4.3+ (optional, for PyMongo utils)`
- `sqlalchemy 2.0+ (optional, for SQLAlchemy utils)`
## Installation
You can simply install odata-v4-query from
[PyPI](https://pypi.org/project/odata-v4-query/):
```bash
pip install odata-v4-query
```
To install all the optional dependencies to use all the ORM/ODM utils:
```bash
pip install odata-v4-query[all]
```
You can also install the dependencies for a specific ORM/ODM util:
```bash
pip install odata-v4-query[beanie]
pip install odata-v4-query[pymongo]
pip install odata-v4-query[sqlalchemy]
```
## Quick Start
```python
from odata_v4_query import ODataQueryParser, ODataFilterParser
# Create parser instance
parser = ODataQueryParser()
# Parse a complete URL
options = parser.parse_url('https://example.com/odata?$count=true&$top=10&$skip=20')
# Parse just the query string
options = parser.parse_query_string("$filter=name eq 'John' and age gt 25")
# Parse filter expressions
filter_parser = ODataFilterParser()
ast = filter_parser.parse("name eq 'John' and age gt 25")
# Evaluate filter expressions
filter_parser.evaluate(ast)
# Filter with nested fields
options = parser.parse_query_string("$filter=user/name eq 'Alice'")
options = parser.parse_query_string("$filter=profile/address/city eq 'Chicago'")
```
## Utility Functions
You to need to install the [required dependencies](#requirements) for the
ORM/ODM you want to use.
> [!NOTE]
> If the `$page` option is used, it is converted to `$skip` and `$top`.
> If `$top` is not provided, it defaults to 100. The `$skip` is computed as
> `(page - 1) * top`. If `$skip` is provided, it is overwritten.
### Beanie
Use the `apply_to_beanie_query()` function to apply options to a Beanie query.
```python
from beanie import Document
from odata_v4_query import ODataQueryParser
from odata_v4_query.utils.beanie import apply_to_beanie_query
class User(Document):
name: str
email: str
age: int
# Create parser instance
parser = ODataQuery_parser()
# Parse a complete URL
options = parser.parse_query_string("$top=10&$skip=20&$filter=name eq 'John'")
# Apply options to a new query
query = apply_to_beanie_query(options, User)
# Apply options to an existing query
query = User.find()
query = apply_to_beanie_query(options, query)
```
Nested field filtering is supported using the `/` separator for accessing nested
document fields. Both single-level and multi-level nesting are supported:
```python
# Single-level: Filter by nested field
options = parser.parse_query_string("$filter=profile/city eq 'Chicago'")
query = apply_to_beanie_query(options, User)
# Multi-level: Filter by deeply nested field
options = parser.parse_query_string("$filter=profile/address/city eq 'Chicago'")
query = apply_to_beanie_query(options, User)
# Use with string functions
options = parser.parse_query_string("$filter=startswith(profile/city, 'Chi')")
query = apply_to_beanie_query(options, User)
```
The `$search` option is only supported if `search_fields` is provided.
```python
options = parser.parse_query_string('$search=John')
# Search "John" in "name" and "email" fields
query = apply_to_beanie_query(options, User, search_fields=['name', 'email'])
```
The `$select` option is only supported if `parse_select` is True.
If `projection_model` is provided, the results are projected with a Pydantic
model, otherwise a dictionary.
```python
from pydantic import BaseModel
class UserProjection(BaseModel):
name: str
email: str
options = parser.parse_query_string("$select=name,email")
# Project as a dictionary (default)
query = apply_to_beanie_query(options, User, parse_select=True)
# Project using a Pydantic model
query = apply_to_beanie_query(
options, User, parse_select=True, projection_model=UserProjection
)
```
> [!NOTE]
> The `$expand` and `$format` options won't be applied.
> You may need to handle them manually. Also, the `substring`, `tolower` and
> `toupper` functions are not supported.
### PyMongo
Use the `get_query_from_options()` function to get a MongoDB query from options
to be applied to a PyMongo query.
```python
from pymongo import MongoClient, ASCENDING, DESCENDING
from odata_v4_query import ODataQueryParser
from odata_v4_query.utils.pymongo import PyMongoQuery, get_query_from_options
client = MongoClient()
db = client['db']
# Create parser instance
parser = ODataQuery_parser()
# Parse a complete URL
options = parser.parse_query_string("$top=10&$skip=20&$filter=name eq 'John'")
# Get a PyMongo query from options
query = get_query_from_options(options)
# Apply query to collection
db.users.find(**query)
# Using keyword arguments
db.users.find(
skip=query.skip,
limit=query.limit,
filter=query.filter,
sort=query.sort,
projection=query.projection,
)
```
Nested field filtering is supported using the `/` separator for accessing nested
document fields. Both single-level and multi-level nesting are supported:
```python
# Single-level: Filter by nested field
options = parser.parse_query_string("$filter=profile/city eq 'Chicago'")
query = get_query_from_options(options)
# Multi-level: Filter by deeply nested field
options = parser.parse_query_string("$filter=profile/address/city eq 'Chicago'")
query = get_query_from_options(options)
# Use with string functions
options = parser.parse_query_string("$filter=contains(profile/city, 'ago')")
query = get_query_from_options(options)
```
The `$search` option is only supported if `search_fields` is provided.
It overrides the `$filter` option.
```python
options = parser.parse_query_string('$search=John')
# Search "John" in "name" and "email" fields
query = get_query_from_options(options, search_fields=['name', 'email'])
```
The `$select` option is only supported if `parse_select` is True.
```python
options = parser.parse_query_string("$select=name,email")
# Parse $select option
query = get_query_from_options(options, parse_select=True)
```
> [!NOTE]
> The `$count`, `$expand` and `$format` options won't be applied.
> You may need to handle them manually. Also, the `substring`, `tolower` and
> `toupper` functions are not supported.
### SQLAlchemy
Use the `apply_to_sqlalchemy_query()` function to apply options to a SQLAlchemy
query.
```python
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from odata_v4_query import ODataQueryParser
from odata_v4_query.utils.sqlalchemy import apply_to_sqlalchemy_query
class User(DeclarativeBase):
name: Mapped[str] = mapped_column()
email: Mapped[str] = mapped_column()
age: Mapped[int] = mapped_column()
# Create parser instance
parser = ODataQuery_parser()
# Parse a complete URL
options = parser.parse_query_string("$top=10&$skip=20&$filter=name eq 'John'")
# Apply options to a new query
query = apply_to_sqlalchemy_query(options, User)
# Apply options to an existing query
query = select(User)
query = apply_to_sqlalchemy_query(options, query)
```
Nested field filtering is supported using the `/` separator for filtering on
related entities. Both single-level and multi-level nesting are supported:
```python
# Single-level: Filter by related entity field
options = parser.parse_query_string("$filter=user/name eq 'Alice'")
query = apply_to_sqlalchemy_query(options, Post)
# Multi-level: Filter by deeply nested field
options = parser.parse_query_string("$filter=user/profile/address/city eq 'Chicago'")
query = apply_to_sqlalchemy_query(options, Post)
# Use with string functions
options = parser.parse_query_string("$filter=tolower(user/name) eq 'alice'")
query = apply_to_sqlalchemy_query(options, Post)
# Multi-level with functions
options = parser.parse_query_string("$filter=startswith(user/profile/address/city, 'Chi')")
query = apply_to_sqlalchemy_query(options, Post)
# Combine with other filters
options = parser.parse_query_string("$filter=user/name eq 'Alice' and rating gt 3")
query = apply_to_sqlalchemy_query(options, Post)
```
The `$search` option is only supported if `search_fields` is provided.
```python
options = parser.parse_query_string('$search=John')
# Search "John" in "name" and "email" fields
query = apply_to_sqlalchemy_query(
options, User, search_fields=['name', 'email']
)
```
The `$expand` option performs a
[joined eager loading](https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#sqlalchemy.orm.joinedload)
using left outer join.
```python
options = parser.parse_query_string('$expand=posts')
# Perform joined eager loading on "posts"
query = apply_to_sqlalchemy_query(options, User)
```
> [!NOTE]
> The `$format` option won't be applied. You may need to handle it
> manually. Also, the `has` and `nor` operators are not supported in SQL,
> so they are converted to a `LIKE` and `NOT` expressions, respectively.
## Contributing
See the [contribution guidelines](CONTRIBUTING.md).
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE)
file for details.
## Support
If you find this project useful, give it a ⭐ on GitHub!