{"id":37072506,"url":"https://github.com/daireto/odata-v4-query","last_synced_at":"2026-01-14T08:30:46.111Z","repository":{"id":280376270,"uuid":"941783987","full_name":"daireto/odata-v4-query","owner":"daireto","description":"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.","archived":false,"fork":false,"pushed_at":"2025-11-09T04:59:55.000Z","size":207,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-09T06:17:23.452Z","etag":null,"topics":["beanie-odm","odata","odata-query-parser","odatav4","pymongo","sqlalchemy","tokenizer-parser"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/odata-v4-query/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/daireto.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-03-03T03:31:14.000Z","updated_at":"2025-06-29T00:19:26.000Z","dependencies_parsed_at":"2025-03-15T08:26:25.461Z","dependency_job_id":"1770c532-2bb5-4852-8582-4028c0b3552e","html_url":"https://github.com/daireto/odata-v4-query","commit_stats":null,"previous_names":["daireto/odata_v4_query","daireto/odata-v4-query"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/daireto/odata-v4-query","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daireto%2Fodata-v4-query","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daireto%2Fodata-v4-query/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daireto%2Fodata-v4-query/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daireto%2Fodata-v4-query/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/daireto","download_url":"https://codeload.github.com/daireto/odata-v4-query/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daireto%2Fodata-v4-query/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28414142,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T08:16:59.381Z","status":"ssl_error","status_checked_at":"2026-01-14T08:13:45.490Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["beanie-odm","odata","odata-query-parser","odatav4","pymongo","sqlalchemy","tokenizer-parser"],"created_at":"2026-01-14T08:30:45.517Z","updated_at":"2026-01-14T08:30:46.080Z","avatar_url":"https://github.com/daireto.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!-- omit in toc --\u003e\n# OData V4 Query\n\n\u003cp align=\"center\"\u003e\n    \u003ca href=\"https://pypi.org/project/odata-v4-query\" target=\"_blank\"\u003e\n        \u003cimg src=\"https://img.shields.io/pypi/pyversions/odata-v4-query\" alt=\"Supported Python versions\"\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://pypi.org/project/odata-v4-query\" target=\"_blank\"\u003e\n        \u003cimg src=\"https://img.shields.io/pypi/v/odata-v4-query\" alt=\"Package version\"\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://github.com/daireto/odata-v4-query/actions\" target=\"_blank\"\u003e\n        \u003cimg src=\"https://github.com/daireto/odata-v4-query/actions/workflows/publish.yml/badge.svg\" alt=\"Publish\"\u003e\n    \u003c/a\u003e\n    \u003ca href='https://coveralls.io/github/daireto/odata-v4-query?branch=main'\u003e\n        \u003cimg src='https://coveralls.io/repos/github/daireto/odata-v4-query/badge.svg?branch=main' alt='Coverage Status' /\u003e\n    \u003c/a\u003e\n    \u003ca href=\"/LICENSE\" target=\"_blank\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/License-MIT-green\" alt=\"License\"\u003e\n    \u003c/a\u003e\n\u003c/p\u003e\n\nA lightweight, simple and fast parser for OData V4 query options supporting\nstandard query parameters. Provides helper functions to apply OData V4 query\noptions to ORM/ODM queries such as SQLAlchemy, PyMongo and Beanie.\n\n\u003c!-- omit in toc --\u003e\n## Table of Contents\n- [Features](#features)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Utility Functions](#utility-functions)\n    - [Beanie](#beanie)\n    - [PyMongo](#pymongo)\n    - [SQLAlchemy](#sqlalchemy)\n- [Contributing](#contributing)\n- [License](#license)\n- [Support](#support)\n\n## Features\n\n- Support for the following OData V4 standard query parameters:\n    - `$count` - Include count of items\n    - `$expand` - Expand related entities\n    - `$filter` - Filter results\n    - `$format` - Response format (json, xml, csv, tsv)\n    - `$orderby` - Sort results\n    - `$search` - Search items\n    - `$select` - Select specific fields\n    - `$skip` - Skip N items\n    - `$top` - Limit to N items\n    - `$page` - Page number\n\n- Comprehensive filter expression support:\n    - Comparison operators: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `in`, `nin`\n    - Logical operators: `and`, `or`, `not`, `nor`\n    - Collection operators: `has`\n    - String functions: `startswith`, `endswith`, `contains`, `substring`, `tolower`, `toupper`\n    - Nested field filtering: `user/name`, `profile/address/city`\n\n- Utility functions to apply options to ORM/ODM queries.\n    - See [utility functions](#utility-functions) for more information.\n\n## Requirements\n\n- `Python 3.10+`\n- `beanie 1.23+ (optional, for Beanie ODM utils)`\n- `pymongo 4.3+ (optional, for PyMongo utils)`\n- `sqlalchemy 2.0+ (optional, for SQLAlchemy utils)`\n\n## Installation\n\nYou can simply install odata-v4-query from\n[PyPI](https://pypi.org/project/odata-v4-query/):\n```bash\npip install odata-v4-query\n```\n\nTo install all the optional dependencies to use all the ORM/ODM utils:\n```bash\npip install odata-v4-query[all]\n```\n\nYou can also install the dependencies for a specific ORM/ODM util:\n```bash\npip install odata-v4-query[beanie]\npip install odata-v4-query[pymongo]\npip install odata-v4-query[sqlalchemy]\n```\n\n## Quick Start\n\n```python\nfrom odata_v4_query import ODataQueryParser, ODataFilterParser\n\n# Create parser instance\nparser = ODataQueryParser()\n\n# Parse a complete URL\noptions = parser.parse_url('https://example.com/odata?$count=true\u0026$top=10\u0026$skip=20')\n\n# Parse just the query string\noptions = parser.parse_query_string(\"$filter=name eq 'John' and age gt 25\")\n\n# Parse filter expressions\nfilter_parser = ODataFilterParser()\nast = filter_parser.parse(\"name eq 'John' and age gt 25\")\n\n# Evaluate filter expressions\nfilter_parser.evaluate(ast)\n\n# Filter with nested fields\noptions = parser.parse_query_string(\"$filter=user/name eq 'Alice'\")\noptions = parser.parse_query_string(\"$filter=profile/address/city eq 'Chicago'\")\n```\n\n## Utility Functions\n\nYou to need to install the [required dependencies](#requirements) for the\nORM/ODM you want to use.\n\n\u003e [!NOTE]\n\u003e If the `$page` option is used, it is converted to `$skip` and `$top`.\n\u003e If `$top` is not provided, it defaults to 100. The `$skip` is computed as\n\u003e `(page - 1) * top`. If `$skip` is provided, it is overwritten.\n\n### Beanie\n\nUse the `apply_to_beanie_query()` function to apply options to a Beanie query.\n\n```python\nfrom beanie import Document\nfrom odata_v4_query import ODataQueryParser\nfrom odata_v4_query.utils.beanie import apply_to_beanie_query\n\nclass User(Document):\n    name: str\n    email: str\n    age: int\n\n# Create parser instance\nparser = ODataQuery_parser()\n\n# Parse a complete URL\noptions = parser.parse_query_string(\"$top=10\u0026$skip=20\u0026$filter=name eq 'John'\")\n\n# Apply options to a new query\nquery = apply_to_beanie_query(options, User)\n\n# Apply options to an existing query\nquery = User.find()\nquery = apply_to_beanie_query(options, query)\n```\n\nNested field filtering is supported using the `/` separator for accessing nested\ndocument fields. Both single-level and multi-level nesting are supported:\n\n```python\n# Single-level: Filter by nested field\noptions = parser.parse_query_string(\"$filter=profile/city eq 'Chicago'\")\nquery = apply_to_beanie_query(options, User)\n\n# Multi-level: Filter by deeply nested field\noptions = parser.parse_query_string(\"$filter=profile/address/city eq 'Chicago'\")\nquery = apply_to_beanie_query(options, User)\n\n# Use with string functions\noptions = parser.parse_query_string(\"$filter=startswith(profile/city, 'Chi')\")\nquery = apply_to_beanie_query(options, User)\n```\n\nThe `$search` option is only supported if `search_fields` is provided.\n\n```python\noptions = parser.parse_query_string('$search=John')\n\n# Search \"John\" in \"name\" and \"email\" fields\nquery = apply_to_beanie_query(options, User, search_fields=['name', 'email'])\n```\n\nThe `$select` option is only supported if `parse_select` is True.\nIf `projection_model` is provided, the results are projected with a Pydantic\nmodel, otherwise a dictionary.\n\n```python\nfrom pydantic import BaseModel\n\nclass UserProjection(BaseModel):\n    name: str\n    email: str\n\noptions = parser.parse_query_string(\"$select=name,email\")\n\n# Project as a dictionary (default)\nquery = apply_to_beanie_query(options, User, parse_select=True)\n\n# Project using a Pydantic model\nquery = apply_to_beanie_query(\n    options, User, parse_select=True, projection_model=UserProjection\n)\n```\n\n\u003e [!NOTE]\n\u003e The `$expand` and `$format` options won't be applied.\n\u003e You may need to handle them manually. Also, the `substring`, `tolower` and\n\u003e `toupper` functions are not supported.\n\n### PyMongo\n\nUse the `get_query_from_options()` function to get a MongoDB query from options\nto be applied to a PyMongo query.\n\n```python\nfrom pymongo import MongoClient, ASCENDING, DESCENDING\nfrom odata_v4_query import ODataQueryParser\nfrom odata_v4_query.utils.pymongo import PyMongoQuery, get_query_from_options\n\nclient = MongoClient()\ndb = client['db']\n\n# Create parser instance\nparser = ODataQuery_parser()\n\n# Parse a complete URL\noptions = parser.parse_query_string(\"$top=10\u0026$skip=20\u0026$filter=name eq 'John'\")\n\n# Get a PyMongo query from options\nquery = get_query_from_options(options)\n\n# Apply query to collection\ndb.users.find(**query)\n\n# Using keyword arguments\ndb.users.find(\n    skip=query.skip,\n    limit=query.limit,\n    filter=query.filter,\n    sort=query.sort,\n    projection=query.projection,\n)\n```\n\nNested field filtering is supported using the `/` separator for accessing nested\ndocument fields. Both single-level and multi-level nesting are supported:\n\n```python\n# Single-level: Filter by nested field\noptions = parser.parse_query_string(\"$filter=profile/city eq 'Chicago'\")\nquery = get_query_from_options(options)\n\n# Multi-level: Filter by deeply nested field\noptions = parser.parse_query_string(\"$filter=profile/address/city eq 'Chicago'\")\nquery = get_query_from_options(options)\n\n# Use with string functions\noptions = parser.parse_query_string(\"$filter=contains(profile/city, 'ago')\")\nquery = get_query_from_options(options)\n```\n\nThe `$search` option is only supported if `search_fields` is provided.\nIt overrides the `$filter` option.\n\n```python\noptions = parser.parse_query_string('$search=John')\n\n# Search \"John\" in \"name\" and \"email\" fields\nquery = get_query_from_options(options, search_fields=['name', 'email'])\n```\n\nThe `$select` option is only supported if `parse_select` is True.\n\n```python\noptions = parser.parse_query_string(\"$select=name,email\")\n\n# Parse $select option\nquery = get_query_from_options(options, parse_select=True)\n```\n\n\u003e [!NOTE]\n\u003e The `$count`, `$expand` and `$format` options won't be applied.\n\u003e You may need to handle them manually. Also, the `substring`, `tolower` and\n\u003e `toupper` functions are not supported.\n\n### SQLAlchemy\n\nUse the `apply_to_sqlalchemy_query()` function to apply options to a SQLAlchemy\nquery.\n\n```python\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nfrom odata_v4_query import ODataQueryParser\nfrom odata_v4_query.utils.sqlalchemy import apply_to_sqlalchemy_query\n\nclass User(DeclarativeBase):\n    name: Mapped[str] = mapped_column()\n    email: Mapped[str] = mapped_column()\n    age: Mapped[int] = mapped_column()\n\n# Create parser instance\nparser = ODataQuery_parser()\n\n# Parse a complete URL\noptions = parser.parse_query_string(\"$top=10\u0026$skip=20\u0026$filter=name eq 'John'\")\n\n# Apply options to a new query\nquery = apply_to_sqlalchemy_query(options, User)\n\n# Apply options to an existing query\nquery = select(User)\nquery = apply_to_sqlalchemy_query(options, query)\n```\n\nNested field filtering is supported using the `/` separator for filtering on\nrelated entities. Both single-level and multi-level nesting are supported:\n\n```python\n# Single-level: Filter by related entity field\noptions = parser.parse_query_string(\"$filter=user/name eq 'Alice'\")\nquery = apply_to_sqlalchemy_query(options, Post)\n\n# Multi-level: Filter by deeply nested field\noptions = parser.parse_query_string(\"$filter=user/profile/address/city eq 'Chicago'\")\nquery = apply_to_sqlalchemy_query(options, Post)\n\n# Use with string functions\noptions = parser.parse_query_string(\"$filter=tolower(user/name) eq 'alice'\")\nquery = apply_to_sqlalchemy_query(options, Post)\n\n# Multi-level with functions\noptions = parser.parse_query_string(\"$filter=startswith(user/profile/address/city, 'Chi')\")\nquery = apply_to_sqlalchemy_query(options, Post)\n\n# Combine with other filters\noptions = parser.parse_query_string(\"$filter=user/name eq 'Alice' and rating gt 3\")\nquery = apply_to_sqlalchemy_query(options, Post)\n```\n\nThe `$search` option is only supported if `search_fields` is provided.\n\n```python\noptions = parser.parse_query_string('$search=John')\n\n# Search \"John\" in \"name\" and \"email\" fields\nquery = apply_to_sqlalchemy_query(\n    options, User, search_fields=['name', 'email']\n)\n```\n\nThe `$expand` option performs a\n[joined eager loading](https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#sqlalchemy.orm.joinedload)\nusing left outer join.\n\n```python\noptions = parser.parse_query_string('$expand=posts')\n\n# Perform joined eager loading on \"posts\"\nquery = apply_to_sqlalchemy_query(options, User)\n```\n\n\u003e [!NOTE]\n\u003e The `$format` option won't be applied. You may need to handle it\n\u003e manually. Also, the `has` and `nor` operators are not supported in SQL,\n\u003e so they are converted to a `LIKE` and `NOT` expressions, respectively.\n\n## Contributing\n\nSee the [contribution guidelines](CONTRIBUTING.md).\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE)\nfile for details.\n\n## Support\n\nIf you find this project useful, give it a ⭐ on GitHub!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaireto%2Fodata-v4-query","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdaireto%2Fodata-v4-query","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaireto%2Fodata-v4-query/lists"}