{"id":14976203,"url":"https://github.com/art1415926535/graphene-sqlalchemy-filter","last_synced_at":"2025-04-05T16:05:07.815Z","repository":{"id":46172967,"uuid":"200573464","full_name":"art1415926535/graphene-sqlalchemy-filter","owner":"art1415926535","description":"Filters for Graphene SQLAlchemy integration","archived":false,"fork":false,"pushed_at":"2023-09-24T15:52:29.000Z","size":250,"stargazers_count":118,"open_issues_count":26,"forks_count":34,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-10-30T08:48:55.912Z","etag":null,"topics":["filters","graphene","graphene-sqlalchemy","graphql","graphql-server","sqlalchemy"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/graphene-sqlalchemy-filter/","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/art1415926535.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-08-05T03:09:46.000Z","updated_at":"2024-02-12T11:26:33.000Z","dependencies_parsed_at":"2024-06-18T18:35:48.802Z","dependency_job_id":"847ec3df-8e8b-4aa5-9745-b2f2a7277387","html_url":"https://github.com/art1415926535/graphene-sqlalchemy-filter","commit_stats":{"total_commits":88,"total_committers":7,"mean_commits":"12.571428571428571","dds":"0.18181818181818177","last_synced_commit":"5d8d903ef671692e90341bd498c6d5b2f60bc232"},"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/art1415926535%2Fgraphene-sqlalchemy-filter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/art1415926535%2Fgraphene-sqlalchemy-filter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/art1415926535%2Fgraphene-sqlalchemy-filter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/art1415926535%2Fgraphene-sqlalchemy-filter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/art1415926535","download_url":"https://codeload.github.com/art1415926535/graphene-sqlalchemy-filter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247361615,"owners_count":20926642,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["filters","graphene","graphene-sqlalchemy","graphql","graphql-server","sqlalchemy"],"created_at":"2024-09-24T13:53:29.910Z","updated_at":"2025-04-05T16:05:07.790Z","avatar_url":"https://github.com/art1415926535.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Graphene-SQLAlchemy-Filter\n\n[![CI](https://github.com/art1415926535/graphene-sqlalchemy-filter/workflows/CI/badge.svg)](https://github.com/art1415926535/graphene-sqlalchemy-filter/actions?query=workflow%3ACI) [![Coverage Status](https://coveralls.io/repos/github/art1415926535/graphene-sqlalchemy-filter/badge.svg?branch=master)](https://coveralls.io/github/art1415926535/graphene-sqlalchemy-filter?branch=master) [![PyPI version](https://badge.fury.io/py/graphene-sqlalchemy-filter.svg)](https://badge.fury.io/py/graphene-sqlalchemy-filter)\n\nFilters for [Graphene SQLAlchemy integration](https://github.com/graphql-python/graphene-sqlalchemy)\n\n![preview](https://github.com/art1415926535/graphene-sqlalchemy-filter/blob/master/preview.gif?raw=true)\n\n# Quick start\n\nCreate a filter and add it to the graphene field.\n```python\nfrom graphene_sqlalchemy_filter import FilterableConnectionField, FilterSet\n\n\nclass UserFilter(FilterSet):\n    is_admin = graphene.Boolean()\n\n    class Meta:\n        model = User\n        fields = {\n            'username': ['eq', 'ne', 'in', 'ilike'],\n            'is_active': [...],  # shortcut!\n        }\n\n    @staticmethod\n    def is_admin_filter(info, query, value):\n        if value:\n            return User.username == 'admin'\n        else:\n            return User.username != 'admin'\n\n\nclass Query(ObjectType):\n    all_users = FilterableConnectionField(UserConnection, filters=UserFilter())\n\n```\n\nNow, we're going to create query.\n```graphql\n{\n  allUsers (\n    filters: {\n      isActive: true,\n      or: [\n        {isAdmin: true},\n        {usernameIn: [\"moderator\", \"cool guy\"]}\n      ]\n    }\n  ){\n    edges {\n      node {\n        id\n        username\n      }\n    }\n  }\n}\n```\n\n---\n\n\n# Filters\n\nFilterSet class must inherit `graphene_sqlalchemy_filter.FilterSet` or your subclass of this class.\n\nThere are three types of filters:  \n  1. [automatically generated filters](#automatically-generated-filters)  \n  1. [simple filters](#simple-filters)  \n  1. [filters that require join](#filters-that-require-join)  \n\n\n## Automatically generated filters\n```python\nclass UserFilter(FilterSet):\n   class Meta:\n       model = User\n       fields = {\n           'username': ['eq', 'ne', 'in', 'ilike'],\n           'is_active': [...],  # shortcut!\n       }\n```\nMetaclass must contain the sqlalchemy model and fields.\n\nAutomatically generated filters must be specified by `fields` variable. \nKey - field name of sqlalchemy model, value - list of expressions (or shortcut).\n\nShortcut (default: `[...]`) will add all the allowed filters for this type of sqlalchemy field (does not work with hybrid property).\n\n| Key            | Description                     | GraphQL postfix |\n|----------------|---------------------------------|-----------------|\n| `eq`           | equal                           |                 |\n| `ne`           | not equal                       | Ne              |\n| `like`         | like                            | Like            |\n| `ilike`        | insensitive like                | Ilike           |\n| `is_null`      | is null                         | IsNull          |\n| `in`           | in                              | In              |\n| `not_in`       | not in                          | NotIn           |\n| `lt`           | less than                       | Lt              |\n| `lte`          | less than or equal              | Lte             |\n| `gt`           | greater than                    | Gt              |\n| `gte`          | greater than or equal           | Gte             |\n| `range`        | in range                        | Range           |\n| `contains`     | contains (PostgreSQL array)     | Contains        |\n| `contained_by` | contained_by (PostgreSQL array) | ContainedBy     |\n| `overlap`      | overlap (PostgreSQL array)      | Overlap         |\n\n## Simple filters\n```python\nclass UserFilter(FilterSet):\n    is_admin = graphene.Boolean()\n\n    @staticmethod\n    def is_admin_filter(info, query, value):\n        if value:\n            return User.username == 'admin'\n        else:\n            return User.username != 'admin'\n```\nEach simple filter has a class variable that passes to GraphQL schema as an input type and function `\u003cfield_name\u003e_filter` that makes filtration.\n\nThe filtration function takes the following arguments:\n  * `info` - ResolveInfo graphene object\n  * `query` - sqlalchemy query (not used in that filters type)\n  * `value` - the value of a filter\n\nThe return value can be any type of sqlalchemy clause. This means that you can return `not_(and_(or_(...), ...))`.\n\nMetaclass is not required if you do not need automatically generated filters.\n\n## Filters that require join\nThis type of filter is the same as [simple filters](#simple-filters) but has a different return type.\n\nThe filtration function should return a new sqlalchemy query and clause (like simple filters).\n\n```python\nclass UserFilter(FilterSet):\n    is_moderator = graphene.Boolean()\n\n    @classmethod\n    def is_moderator_filter(cls, info, query, value):\n        membership = cls.aliased(query, Membership, name='is_moderator')\n  \n        query = query.outerjoin(\n            membership,\n            and_(\n                User.id == membership.user_id,\n                membership.is_moderator.is_(True),\n            ),\n        )\n\n        if value:\n            filter_ = membership.id.isnot(None)\n        else:\n            filter_ = membership.id.is_(None)\n\n        return query, filter_\n```\n\n### Model aliases\n\nThe function `cls.aliased(query, model, name='...')` returns [sqlalchemy alias](https://docs.sqlalchemy.org/en/13/orm/query.html#sqlalchemy.orm.aliased) from the query. It has one differing parameter - `query` (SQLAlchemy Query object). Other arguments are the same as [sqlalchemy.orm.aliased](https://docs.sqlalchemy.org/en/13/orm/query.html#sqlalchemy.orm.aliased).\n\nIdentical joins will be skipped by sqlalchemy.\n\n\u003e Changed in version 1.7: The first parameter is now a query.\n\n\n# Features\n\n## Filter registration and nested fields filters\n\nFilters can be registered for each SQLAlchemy model in a subclass of `FilterableConnectionField`.\n\nRegister your filters by inheriting `FilterableConnectionField` and setting `filters` (key - SQLAlchemy model, value - FilterSet object).\n\n```python\nclass CustomField(FilterableConnectionField):\n    filters = {\n        User: UserFilter(),\n    }\n```\n\nOverriding `SQLAlchemyObjectType.connection_field_factory` allows you to generate nested connections with filters.\n\n```python\nclass UserNode(SQLAlchemyObjectType):\n    class Meta:\n        model = User\n        interfaces = (Node,)\n        connection_field_factory = CustomField.factory\n```\n\n**Important:**\n  1. pagination (first/after, last/before) are performed by python (keep this in mind when working with large amounts of data)\n  1. nested filters work by dataloaders\n  1. this module optimizes one-to-many relationships, to optimize many-to-one relationships use [sqlalchemy_bulk_lazy_loader](https://github.com/operator/sqlalchemy_bulk_lazy_loader)\n  1. nested filters require `graphene_sqlalchemy\u003e=2.1.2`\n\n\n### Example\n```python\n# Filters\n\nclass UserFilter(FilterSet):\n   class Meta:\n       model = User\n       fields = {'is_active': [...]}\n       \n\n\nclass CustomField(FilterableConnectionField):\n    filters = {\n        User: UserFilter(),\n    }\n\n\n# Nodes\n\nclass UserNode(SQLAlchemyObjectType):\n    class Meta:\n        model = User\n        interfaces = (Node,)\n        connection_field_factory = CustomField.factory\n\n\nclass GroupNode(SQLAlchemyObjectType):\n    class Meta:\n        model = Group\n        interfaces = (Node,)\n        connection_field_factory = CustomField.factory\n\n\n# Connections\n\nclass UserConnection(Connection):\n    class Meta:\n        node = UserNode\n\n\nclass GroupConnection(Connection):\n    class Meta:\n        node = GroupNode\n\n\n# Query\n\nclass Query(ObjectType):\n    all_users = CustomField(UserConnection)\n    all_groups = CustomField(GroupConnection)\n\n```\n\n```graphql\n{\n  allUsers (filters: {isActive: true}){\n    edges { node { id } }\n  }\n  allGroups {\n    edges {\n      node {\n        users (filters: {isActive: true}) {\n          edges { node { id } }\n        }\n      }\n    }\n  }\n}\n```\n\n## Rename GraphQL filter field\n\n```python\nclass CustomField(FilterableConnectionField):\n    filter_arg = 'where'\n\n\nclass Query(ObjectType):\n    all_users = CustomField(UserConnection, where=UserFilter())\n    all_groups = FilterableConnectionField(GroupConnection, filters=GroupFilter())\n\n```\n\n```graphql\n{\n  allUsers (where: {isActive: true}){\n    edges { node { id } }\n  }\n  allGroups (filters: {nameIn: [\"python\", \"development\"]}){\n    edges { node { id } }\n  }\n}\n```\n\n\n## Rename expression\n\n```python\nclass BaseFilter(FilterSet):\n    GRAPHQL_EXPRESSION_NAMES = dict(\n        FilterSet.GRAPHQL_EXPRESSION_NAMES,\n        **{'eq': 'equal', 'not': 'i_never_asked_for_this'}\n    )\n\n    class Meta:\n        abstract = True\n\n\nclass UserFilter(BaseFilter):\n    class Meta:\n        model = User\n        fields = {'first_name': ['eq'], 'last_name': ['eq']}\n\n```\n\n```graphql\n{\n  allUsers (filters: {iNeverAskedForThis: {firstNameEqual: \"Adam\", lastNameEqual: \"Jensen\"}}){\n    edges { node { id } }\n  }\n}\n```\n\n\n## Custom shortcut value\n\n```python\nclass BaseFilter(FilterSet):\n    ALL = '__all__'\n\n    class Meta:\n        abstract = True\n\n\nclass UserFilter(BaseFilter):\n    class Meta:\n        model = User\n        fields = {'username': '__all__'}\n\n```\n\n\n## Localization of documentation\n\n```python\nclass BaseFilter(FilterSet):\n    DESCRIPTIONS = {\n        'eq': 'Полностью совпадает.',\n        'ne': 'Не совпадает.',\n        'like': 'Регистрозависимая проверка строки по шлабону.',\n        'ilike': 'Регистронезависимая проверка строки по шлабону.',\n        'regexp': 'Регистрозависимая проверка строки по регулярному выражению.',\n        'is_null': 'Равно ли значение `null`. Принемает `true` или `false`.',\n        'in': 'Проверка вхождения в список.',\n        'not_in': 'Проверка не вхождения в список.',\n        'lt': 'Меньше, чем указанное значение.',\n        'lte': 'Меньше или равно указанному значению.',\n        'gt': 'Больше, чем указанное значение.',\n        'gte': 'Больше или равно указанному значению.',\n        'range': 'Значение входит в диапазон значений.',\n        'and': 'Объединение фильтров с помощью ``AND``.',\n        'or': 'Объединение фильтров с помощью ``OR``.',\n        'not': 'Отрицание указанных фильтров.',\n    }\n\n    class Meta:\n        abstract = True\n\n```\n\n\n## Custom expression\n\n```python\ndef today_filter(field, value: bool):\n    today = func.date(field) == date.today()\n    return today if value else not_(today)\n\n\nclass BaseFilter(FilterSet):\n    # Add expression.\n    TODAY = 'today'\n\n    EXTRA_EXPRESSIONS = {\n        'today': {\n            # Add the name of the expression in GraphQL.\n            'graphql_name': 'today',\n            # Update allowed filters (used by shortcut).\n            'for_types': [types.Date, types.DateTime],\n            # Add a filtering function (takes the sqlalchemy field and value).\n            'filter': today_filter,\n            # Add the GraphQL input type. Column type by default.\n            'input_type': (\n                lambda type_, nullable, doc: graphene.Boolean(nullable=False)\n            ),\n            # Description for the GraphQL schema.\n            'description': 'It is today.',\n        }\n    }\n\n    class Meta:\n        abstract = True\n\n\nclass PostFilter(BaseFilter):\n    class Meta:\n        model = Post\n        fields = {'created': ['today'], 'updated': [...]}\n```\n\n```graphql\n{\n  allPosts (filters: {createdToday: false, updatedToday: true}){\n    edges { node { id } }\n  }\n}\n```\n\n\n## Custom column types\n`ALLOWED_FILTERS` and `EXTRA_ALLOWED_FILTERS` only affect shortcut.\n\nIf you do not use the shortcut, you can skip the next steps described in the section.\n\n```python\nclass MyString(types.String):\n    pass\n\n\nclass BaseFilter(FilterSet):\n    # You can override all allowed filters\n    # ALLOWED_FILTERS = {types.Integer: ['eq']}\n    \n    # Or add new column type\n    EXTRA_ALLOWED_FILTERS = {MyString: ['eq']}\n\n    class Meta:\n        abstract = True\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fart1415926535%2Fgraphene-sqlalchemy-filter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fart1415926535%2Fgraphene-sqlalchemy-filter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fart1415926535%2Fgraphene-sqlalchemy-filter/lists"}