Django Relay Endpoint
=====
"Django Relay Endpoint" is a Django addon that automatically configures a customizable graphql endpoint from models.
The addon is made for "graphene_django".
Table of Contents
-----
- [Django Relay Endpoint](#django-relay-endpoint)
- [Table of Contents](#table-of-contents)
- [Requirements](#requirements)
- [Installation](#installation)
- [How to use](#how-to-use)
- [Adding custom query and mutation types](#adding-custom-query-and-mutation-types)
- [Configuring NodeType subclasses](#configuring-nodetype-subclasses)
- [Validators](#validators)
- [Permissions](#permissions)
- [Useful subclasses and tools](#useful-subclasses-and-tools)
- [License](#license)
- [Documentation](#documentation)
- [Tip the author](#tip-the-author)
- [Client](#client)
- [To-Do](#to-do)
Requirements
-----
Python 3.8 or higher
Django 4.2.7 or higher
It is possible that the addon will work with lower code setup. Keep in mind that the code uses `__init_subclass__` method and format string. It was tested made with the following code setup
- Python 3.11.2
- Django >= 4.2.7
- graphene-django >= 3.3.0
- graphene-file-upload >= 1.3.0
- django_filter >= 23.3.0
This package will also install `graphene-django`, `graphene-file-upload` and `django_filter`.
Installation
-----
Install using pip:
```
pip install django-graphene-endpoint
```
How to use
-----
1. Add `'graphene_django'` to your project's `settings.py`:
```
INSTALLED_APPS = [
# ... other apps and addons
'graphene_django',
]
```
2. Declare your NodeTypes and pass it to the SchemaConfigurator to get the schema. e.g.
```
# endpoint.py
from django_relay_endpoint import NodeType, SchemaConfigurator
class AuthorType(NodeType):
@staticmethod
def get_queryset(object_type, queryset, info):
return queryset.filter(age__gte=35) # filter authors with age higher than 35
class Meta:
model = 'my_app.Author'
fields = ['id', 'name', 'age', 'books']
filter_fields = {
'id': ('exact',),
'name': ('iexact', 'icontains'),
}
extra_kwargs: {
'name': {
"required": True,
},
'age': {
"required": True,
}
}
class BookType(NodeType):
class Meta:
model = 'my_app.Book'
fields = ['id', 'name', 'authors']
schema = SchemaConfigurator([
AuthorType,
BookType,
]).schema()
```
3. In your urls.py add the endpoint
```
# urls.py
from graphene_file_upload.django import FileUploadGraphQLView
from my_app.endpoint import schema
from django.views.decorators.csrf import csrf_exempt
urlpatterns = [
# ... other urls
path("graphql_dashboard_v1", csrf_exempt(FileUploadGraphQLView.as_view(graphiql=True, schema=schema))),
]
```
It uses `FileUploadGraphQLView` from `graphene_file_upload.django` to support file uploads.
Adding custom query and mutation types
-----
The `SchemaConfigurator` **instance** has `query` and `mutation` properties of type List, when instantiated.
Custom object types can be appended to the `query` and `mutation` properties: e.g.
```
# let's imagine we have created a mutation that handles login
from my_app.models import Book
class AuthType(Mutation): ...
# and we have BookType configured with NodeType
class BookType(NodeType):
class Meta:
model = Book
fields = '__all__'
schema = SchemaConfigurator([
BookType,
]) # will instantiate the SchemaConfigurator with rootfields form BookType.
schema.mutations.append(AuthType) # adds AuthType to mutations.
schema = schema.schema() # overwrite schema with actual schema. This will create the actual schema by extending all types and return the schema.
```
Configuring NodeType subclasses
-----
A subclass of NodeType can be configured via its Meta class.
Available options are as follows:
**Following options can be configured on class Meta**:
- **model**: (Union[str, Type[models.Model]]) - a string composed of 'app_name.model_name' or actual django model.
- **fields**: List[str] | Literal["__all__"] - an explicit list of field names or '__all__'.
- **query_root_name**: str | None - a root field name. Defaults to lowered snake-case model._meta.verbose_name.
- **query_root_name_plural**: str | None - a root field name. Defaults to lowered snake-case model._meta.verbose_name_plural.
- **filter_fields**: Union[Dict[str, List[str]], List[str]] - fielter_fields configurations. see <https://docs.graphene-python.org/projects/django/en/latest/filtering/#filterable-fields>.
- **filterset_class**: FilterSet - a filterset_class. see <https://docs.graphene-python.org/projects/django/en/latest/filtering/#custom-filtersets>.
- **query_operations**: Literal["list", "detail"] - whether the query root field should be configured for single and multiple results. Defaults to `["list", "detail"]` which means both will be configured.
- **object_type_name**: str | None - The classname of the DjangoObjectType that will be configured. Defaults to camel-case `AppNameModelNameType`.
- **mutation_operations**: Literal["create", "update", "delete"] - similar to query_operations, this limits the root field configuration, defaults to `["create", "update", "delete"]`.
- **extra_kwargs**: Dict[str, Dict[str, Any]] - the mutation type fields are configured via assigned django form field; this option is similar to rest framework serializer `extra_kwargs`, which is a dictionary of field_names mapped to a dictionary of django form field kwargs. The configurator automatically maps the field to the respective form field: for field mapping see <https://docs.djangoproject.com/en/4.2/topics/forms/modelforms/#field-types>. For relations, it maps the fields to `graphene.List(graphene.ID, **field_kwargs)` `and graphene.ID(**field_kwargs)`, it will also infer the `required` parameter value from the declared `allow_blank` and `allow_null` parameters of the respective model.field.
- **field_validators**: Dict[str, List[Callable]] - a dictionary of field_names mapped to the list of validators: see [Validators](#Validators).
- **non_field_validators**: List[Callable] - list of validators: see [Validators](#Validators).
- **success_keyword**: str - a success keyword for mutation responses. by defualt it is 'success'.
- **input_field_name**: str - a Input field name for mutations. Defaults to 'data'.
- **return_field_name**: str - the field name on the response on create and update mutations, if none provided, model._meta.model_name will be used.
- **permissions**: List[str] - A list of permission names, defaults to empty list, i.e. no permissions will be checked.
- **permission_classes**: List[Type[BasePermission]] - A list of permission classes. see [Permissions](#Permissions).
**Following fields can be configured on the subclass of the NodeType**:
- **get_queryset**: Callable - a static get_queryset method. Important! this method should be declared as staticmethod, it will be returned with the configured subclass of DjangoObjectType, queryset and info. It behaves as overwrite of get_queryset method, but is a staticmethod. See the example in [How to use](#how-to-use).
Validators
-----
A validator passed to `field_validators` or `non_field_validators` is a function that takes the following arguments:
- **data**: the field value for field_validators and whole data object for non_field_validators
- **not_updated_model_instance**: the instance with the state before merging data with the instance
- **info**: the graphene resolve info object instance.
Permissions
-----
We have extended DjangoObjectType and ClientIDMutation to support string permissions and class based permissions for queryset and object level permission checks.
Class based permissions extend custom `BasePermission` class, which implements `has_permission(self, info) -> bool` and `has_object_permission(self, info, obj) -> bool` methods. If the class returns `False` a permission-denied error will be raised. Following default permission classes can be found in graphene_relay_endpoint:
- **AllowAny**: This class is intended only for explicit declaration. It does nothing similar to the same permission in REST framework
- **IsAuthenticated**: Checks for authentication.
- **IsAdminUser**: Checks for admin privilege.
- **IsAuthenticatedOrReadOnly**: Limits mutation operations to authenticated users.
- **BasePermission**: A base class to subclass for custom permission classes.
Useful subclasses and tools
-----
The addon comes with builtin DjangoClientIDMutation abstract subclass, which implements following methods
- **get_queryset**: same as on graphen_django.DjangoObjectType
- **get_node**: same as on graphen_django.DjangoObjectType
- **create_node**: creates an empty instance of the given mode
- **validate**: validates data via 'field_validators' and 'non_field_validators' supplied with the subclass of NodeType.
- **update_instance**: set's the values on the instance from data. For to-many relations it uses the `add_<field_name>` and `remove_<field_name>` convention. First it adds than it removes. The client can pass both, and the relations will be added and removed consecutively before being saved.
N.B. DjangoClientIDMutation does not implement a `mutate_and_get_payload` classmethod, the developer must implement it on a subclass.
License
-----
See the MIT licens in the LICENSE file in the project.
Documentation
-----
The addon is pretty simple. The [How to use](#how-to-use) and [Configuring NodeType subclasses](#configuring-nodetype-subclasses) explains it all. Each piece of code is also documented with dockstrings and has respective type hints.
For additional information read the respective documentation:
- **graphene_django**: <https://docs.graphene-python.org/projects/django/>
- **graphene-file-upload**: <https://github.com/lmcgartland/graphene-file-upload>
Tip the author
-----
If this project has facilitated your job, saved time spent on boilerplate code and pain of standardizing and debugging a relay style endpoint, consider tipping (donating) the author with some crypto:
**Bitcoin**: `3N5ot3DA2vSLwEqhjTGhfVnGaAuQoWBrCf`
Thank you!
Client
-----
An ember client with ember-data style encapsulation layer is on the way.
To-Do
-----
- Default Login configurator
Raw data
{
"_id": null,
"home_page": "https://github.com/Elawphant/django_relay_endpoint",
"name": "django-graphene-endpoint",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": "",
"keywords": "",
"author": "Gevorg Hakobyan",
"author_email": "gevorg.hakobyan@elawphant.am",
"download_url": "https://files.pythonhosted.org/packages/6e/c0/4fcb7c3a08fce8c867d800b17c9c69140e229349c919d2c601285abd1368/django_graphene_endpoint-1.1.tar.gz",
"platform": null,
"description": "Django Relay Endpoint\n=====\n\"Django Relay Endpoint\" is a Django addon that automatically configures a customizable graphql endpoint from models.\nThe addon is made for \"graphene_django\".\n\n\nTable of Contents\n-----\n- [Django Relay Endpoint](#django-relay-endpoint)\n - [Table of Contents](#table-of-contents)\n - [Requirements](#requirements)\n - [Installation](#installation)\n - [How to use](#how-to-use)\n - [Adding custom query and mutation types](#adding-custom-query-and-mutation-types)\n - [Configuring NodeType subclasses](#configuring-nodetype-subclasses)\n - [Validators](#validators)\n - [Permissions](#permissions)\n - [Useful subclasses and tools](#useful-subclasses-and-tools)\n - [License](#license)\n - [Documentation](#documentation)\n - [Tip the author](#tip-the-author)\n - [Client](#client)\n - [To-Do](#to-do)\n\n\nRequirements\n-----\nPython 3.8 or higher\nDjango 4.2.7 or higher\n\nIt is possible that the addon will work with lower code setup. Keep in mind that the code uses `__init_subclass__` method and format string. It was tested made with the following code setup\n- Python 3.11.2\n- Django >= 4.2.7\n- graphene-django >= 3.3.0\n- graphene-file-upload >= 1.3.0\n- django_filter >= 23.3.0\n\n\nThis package will also install `graphene-django`, `graphene-file-upload` and `django_filter`.\n\n\nInstallation\n-----\nInstall using pip:\n\n```\npip install django-graphene-endpoint\n```\n\nHow to use\n-----\n1. Add `'graphene_django'` to your project's `settings.py`:\n\n```\nINSTALLED_APPS = [\n # ... other apps and addons\n 'graphene_django',\n]\n\n```\n\n\n2. Declare your NodeTypes and pass it to the SchemaConfigurator to get the schema. e.g.\n\n```\n# endpoint.py\nfrom django_relay_endpoint import NodeType, SchemaConfigurator\n\nclass AuthorType(NodeType):\n @staticmethod\n def get_queryset(object_type, queryset, info):\n return queryset.filter(age__gte=35) # filter authors with age higher than 35\n\n class Meta:\n model = 'my_app.Author'\n fields = ['id', 'name', 'age', 'books']\n filter_fields = {\n 'id': ('exact',),\n 'name': ('iexact', 'icontains'),\n }\n extra_kwargs: {\n 'name': {\n \"required\": True,\n },\n 'age': {\n \"required\": True,\n }\n }\n\nclass BookType(NodeType):\n class Meta:\n model = 'my_app.Book'\n fields = ['id', 'name', 'authors']\n\n\nschema = SchemaConfigurator([\n AuthorType,\n BookType,\n]).schema()\n\n```\n\n3. In your urls.py add the endpoint\n```\n# urls.py\nfrom graphene_file_upload.django import FileUploadGraphQLView\nfrom my_app.endpoint import schema\nfrom django.views.decorators.csrf import csrf_exempt\n\nurlpatterns = [\n # ... other urls\n path(\"graphql_dashboard_v1\", csrf_exempt(FileUploadGraphQLView.as_view(graphiql=True, schema=schema))),\n]\n\n```\nIt uses `FileUploadGraphQLView` from `graphene_file_upload.django` to support file uploads.\n\n\nAdding custom query and mutation types\n-----\nThe `SchemaConfigurator` **instance** has `query` and `mutation` properties of type List, when instantiated.\n\nCustom object types can be appended to the `query` and `mutation` properties: e.g.\n\n```\n# let's imagine we have created a mutation that handles login\nfrom my_app.models import Book\n\nclass AuthType(Mutation): ...\n\n# and we have BookType configured with NodeType \nclass BookType(NodeType):\n class Meta:\n model = Book\n fields = '__all__'\n\nschema = SchemaConfigurator([\n BookType,\n]) # will instantiate the SchemaConfigurator with rootfields form BookType.\n\nschema.mutations.append(AuthType) # adds AuthType to mutations.\n\nschema = schema.schema() # overwrite schema with actual schema. This will create the actual schema by extending all types and return the schema.\n\n```\n\n\nConfiguring NodeType subclasses\n-----\n\nA subclass of NodeType can be configured via its Meta class.\n\nAvailable options are as follows: \n\n**Following options can be configured on class Meta**:\n- **model**: (Union[str, Type[models.Model]]) - a string composed of 'app_name.model_name' or actual django model.\n- **fields**: List[str] | Literal[\"__all__\"] - an explicit list of field names or '__all__'.\n- **query_root_name**: str | None - a root field name. Defaults to lowered snake-case model._meta.verbose_name.\n- **query_root_name_plural**: str | None - a root field name. Defaults to lowered snake-case model._meta.verbose_name_plural.\n- **filter_fields**: Union[Dict[str, List[str]], List[str]] - fielter_fields configurations. see <https://docs.graphene-python.org/projects/django/en/latest/filtering/#filterable-fields>.\n- **filterset_class**: FilterSet - a filterset_class. see <https://docs.graphene-python.org/projects/django/en/latest/filtering/#custom-filtersets>.\n- **query_operations**: Literal[\"list\", \"detail\"] - whether the query root field should be configured for single and multiple results. Defaults to `[\"list\", \"detail\"]` which means both will be configured.\n- **object_type_name**: str | None - The classname of the DjangoObjectType that will be configured. Defaults to camel-case `AppNameModelNameType`.\n- **mutation_operations**: Literal[\"create\", \"update\", \"delete\"] - similar to query_operations, this limits the root field configuration, defaults to `[\"create\", \"update\", \"delete\"]`.\n- **extra_kwargs**: Dict[str, Dict[str, Any]] - the mutation type fields are configured via assigned django form field; this option is similar to rest framework serializer `extra_kwargs`, which is a dictionary of field_names mapped to a dictionary of django form field kwargs. The configurator automatically maps the field to the respective form field: for field mapping see <https://docs.djangoproject.com/en/4.2/topics/forms/modelforms/#field-types>. For relations, it maps the fields to `graphene.List(graphene.ID, **field_kwargs)` `and graphene.ID(**field_kwargs)`, it will also infer the `required` parameter value from the declared `allow_blank` and `allow_null` parameters of the respective model.field.\n- **field_validators**: Dict[str, List[Callable]] - a dictionary of field_names mapped to the list of validators: see [Validators](#Validators).\n- **non_field_validators**: List[Callable] - list of validators: see [Validators](#Validators).\n- **success_keyword**: str - a success keyword for mutation responses. by defualt it is 'success'. \n- **input_field_name**: str - a Input field name for mutations. Defaults to 'data'.\n- **return_field_name**: str - the field name on the response on create and update mutations, if none provided, model._meta.model_name will be used.\n- **permissions**: List[str] - A list of permission names, defaults to empty list, i.e. no permissions will be checked.\n- **permission_classes**: List[Type[BasePermission]] - A list of permission classes. see [Permissions](#Permissions).\n\n**Following fields can be configured on the subclass of the NodeType**:\n- **get_queryset**: Callable - a static get_queryset method. Important! this method should be declared as staticmethod, it will be returned with the configured subclass of DjangoObjectType, queryset and info. It behaves as overwrite of get_queryset method, but is a staticmethod. See the example in [How to use](#how-to-use).\n\n\nValidators\n-----\nA validator passed to `field_validators` or `non_field_validators` is a function that takes the following arguments:\n- **data**: the field value for field_validators and whole data object for non_field_validators \n\n- **not_updated_model_instance**: the instance with the state before merging data with the instance \n\n- **info**: the graphene resolve info object instance.\n\n\nPermissions\n-----\nWe have extended DjangoObjectType and ClientIDMutation to support string permissions and class based permissions for queryset and object level permission checks.\n\nClass based permissions extend custom `BasePermission` class, which implements `has_permission(self, info) -> bool` and `has_object_permission(self, info, obj) -> bool` methods. If the class returns `False` a permission-denied error will be raised. Following default permission classes can be found in graphene_relay_endpoint:\n- **AllowAny**: This class is intended only for explicit declaration. It does nothing similar to the same permission in REST framework\n- **IsAuthenticated**: Checks for authentication.\n- **IsAdminUser**: Checks for admin privilege.\n- **IsAuthenticatedOrReadOnly**: Limits mutation operations to authenticated users.\n- **BasePermission**: A base class to subclass for custom permission classes.\n\n\nUseful subclasses and tools\n-----\nThe addon comes with builtin DjangoClientIDMutation abstract subclass, which implements following methods\n- **get_queryset**: same as on graphen_django.DjangoObjectType\n- **get_node**: same as on graphen_django.DjangoObjectType\n- **create_node**: creates an empty instance of the given mode\n- **validate**: validates data via 'field_validators' and 'non_field_validators' supplied with the subclass of NodeType.\n- **update_instance**: set's the values on the instance from data. For to-many relations it uses the `add_<field_name>` and `remove_<field_name>` convention. First it adds than it removes. The client can pass both, and the relations will be added and removed consecutively before being saved. \n\nN.B. DjangoClientIDMutation does not implement a `mutate_and_get_payload` classmethod, the developer must implement it on a subclass.\n\n\n\nLicense\n-----\nSee the MIT licens in the LICENSE file in the project.\n\n\nDocumentation\n-----\nThe addon is pretty simple. The [How to use](#how-to-use) and [Configuring NodeType subclasses](#configuring-nodetype-subclasses) explains it all. Each piece of code is also documented with dockstrings and has respective type hints.\nFor additional information read the respective documentation: \n- **graphene_django**: <https://docs.graphene-python.org/projects/django/>\n- **graphene-file-upload**: <https://github.com/lmcgartland/graphene-file-upload>\n\n\nTip the author\n-----\nIf this project has facilitated your job, saved time spent on boilerplate code and pain of standardizing and debugging a relay style endpoint, consider tipping (donating) the author with some crypto:\n\n**Bitcoin**: `3N5ot3DA2vSLwEqhjTGhfVnGaAuQoWBrCf`\n\nThank you!\n\n\nClient\n-----\nAn ember client with ember-data style encapsulation layer is on the way. \n\nTo-Do\n-----\n- Default Login configurator\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Django addon to automatically configure graphql endpoints based on Django models.",
"version": "1.1",
"project_urls": {
"Homepage": "https://github.com/Elawphant/django_relay_endpoint"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "5b73b8b2cb154523a73870ed43603d0a6e283a07125e4dd367de9ed00dcd3a9a",
"md5": "8136f058bb7fa1834e40474af9195593",
"sha256": "a8b86401be5cc00f6439cf8e4f7b3ca08f53825a7a8f04209ea2404bbb09c95a"
},
"downloads": -1,
"filename": "django_graphene_endpoint-1.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "8136f058bb7fa1834e40474af9195593",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 26442,
"upload_time": "2023-11-21T09:23:55",
"upload_time_iso_8601": "2023-11-21T09:23:55.064606Z",
"url": "https://files.pythonhosted.org/packages/5b/73/b8b2cb154523a73870ed43603d0a6e283a07125e4dd367de9ed00dcd3a9a/django_graphene_endpoint-1.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "6ec04fcb7c3a08fce8c867d800b17c9c69140e229349c919d2c601285abd1368",
"md5": "2e5cd55d59020f0c2485a58a801f4991",
"sha256": "634b51428d1d015851c8d0de35b1b3f0df1664821d9189cc346d54bce126db84"
},
"downloads": -1,
"filename": "django_graphene_endpoint-1.1.tar.gz",
"has_sig": false,
"md5_digest": "2e5cd55d59020f0c2485a58a801f4991",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 18683,
"upload_time": "2023-11-21T09:23:57",
"upload_time_iso_8601": "2023-11-21T09:23:57.020264Z",
"url": "https://files.pythonhosted.org/packages/6e/c0/4fcb7c3a08fce8c867d800b17c9c69140e229349c919d2c601285abd1368/django_graphene_endpoint-1.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-11-21 09:23:57",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Elawphant",
"github_project": "django_relay_endpoint",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"requirements": [],
"lcname": "django-graphene-endpoint"
}