
commit
0434899b4e
82 changed files with 3891 additions and 0 deletions
@ -0,0 +1,81 @@
|
||||
# Created by https://www.gitignore.io |
||||
|
||||
### Python ### |
||||
# Byte-compiled / optimized / DLL files |
||||
__pycache__/ |
||||
*.py[cod] |
||||
|
||||
# C extensions |
||||
*.so |
||||
|
||||
# Distribution / packaging |
||||
.Python |
||||
env/ |
||||
build/ |
||||
develop-eggs/ |
||||
dist/ |
||||
downloads/ |
||||
eggs/ |
||||
.eggs/ |
||||
lib/ |
||||
lib64/ |
||||
parts/ |
||||
sdist/ |
||||
var/ |
||||
*.egg-info/ |
||||
.installed.cfg |
||||
*.egg |
||||
|
||||
# PyInstaller |
||||
# Usually these files are written by a python script from a template |
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it. |
||||
*.manifest |
||||
*.spec |
||||
|
||||
# Installer logs |
||||
pip-log.txt |
||||
pip-delete-this-directory.txt |
||||
|
||||
# Unit test / coverage reports |
||||
htmlcov/ |
||||
.tox/ |
||||
.coverage |
||||
.coverage.* |
||||
.cache |
||||
nosetests.xml |
||||
coverage.xml |
||||
*,cover |
||||
|
||||
# Translations |
||||
*.mo |
||||
*.pot |
||||
|
||||
# Django stuff: |
||||
*.log |
||||
|
||||
# Sphinx documentation |
||||
docs/_build/ |
||||
|
||||
# PyBuilder |
||||
target/ |
||||
|
||||
|
||||
/tests/django.sqlite |
||||
|
||||
/graphene/index.json |
||||
/graphene/meta.json |
||||
|
||||
/meta.json |
||||
/index.json |
||||
|
||||
/docs/playground/graphene-js/pypyjs-release-nojit/ |
||||
/docs/static/playground/lib |
||||
|
||||
/docs/static/playground |
||||
|
||||
# PyCharm |
||||
.idea |
||||
|
||||
# Databases |
||||
*.sqlite3 |
||||
.vscode |
@ -0,0 +1,49 @@
|
||||
language: python |
||||
sudo: false |
||||
python: |
||||
- 2.7 |
||||
- 3.4 |
||||
- 3.5 |
||||
- pypy |
||||
before_install: |
||||
- | |
||||
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then |
||||
export PYENV_ROOT="$HOME/.pyenv" |
||||
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then |
||||
cd "$PYENV_ROOT" && git pull |
||||
else |
||||
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" |
||||
fi |
||||
export PYPY_VERSION="4.0.1" |
||||
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION" |
||||
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" |
||||
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" |
||||
fi |
||||
install: |
||||
- | |
||||
if [ "$TEST_TYPE" = build ]; then |
||||
pip install pytest pytest-cov pytest-benchmark coveralls six pytest-django mock django-filter |
||||
pip install -e . |
||||
python setup.py develop |
||||
elif [ "$TEST_TYPE" = lint ]; then |
||||
pip install flake8 |
||||
fi |
||||
script: |
||||
- | |
||||
if [ "$TEST_TYPE" = lint ]; then |
||||
echo "Checking Python code lint." |
||||
flake8 graphene_django |
||||
exit |
||||
elif [ "$TEST_TYPE" = build ]; then |
||||
py.test --cov=graphene_django graphene_django examples |
||||
fi |
||||
after_success: |
||||
- | |
||||
if [ "$TEST_TYPE" = build ]; then |
||||
coveralls |
||||
fi |
||||
matrix: |
||||
fast_finish: true |
||||
include: |
||||
- python: '2.7' |
||||
env: TEST_TYPE=lint |
@ -0,0 +1,72 @@
|
||||
You are in the `next` unreleased version of Graphene-Django (`1.0.dev`). |
||||
Please read [UPGRADE-v1.0.md](https://github.com/graphql-python/graphene/blob/master/UPGRADE-v1.0.md) to learn how to upgrade. |
||||
|
||||
--- |
||||
|
||||
#  [Graphene-Django](http://graphene-python.org) [](https://travis-ci.org/graphql-python/graphene-django) [](https://badge.fury.io/py/graphene-django) [](https://coveralls.io/github/graphql-python/graphene-django?branch=master) |
||||
|
||||
|
||||
[Graphene](http://graphene-python.org) is a Python library for building GraphQL schemas/types fast and easily. |
||||
|
||||
- **Easy to use:** Graphene helps you use GraphQL in Python without effort. |
||||
- **Relay:** Graphene has builtin support for Relay |
||||
- **Django:** Automatic *Django model* mapping to Graphene Types. Check a fully working [Django](http://github.com/graphql-python/swapi-graphene) implementation |
||||
|
||||
Graphene also supports *SQLAlchemy*! |
||||
|
||||
*What is supported in this Python version?* **Everything**: Interfaces, ObjectTypes, Scalars, Unions and Relay (Nodes, Connections), in addition to queries, mutations and subscriptions. |
||||
|
||||
**NEW**!: [Try graphene online](http://graphene-python.org/playground/) |
||||
|
||||
## Installation |
||||
|
||||
For instaling graphene, just run this command in your shell |
||||
|
||||
```bash |
||||
pip install "graphene-django>=1.0.dev" |
||||
``` |
||||
|
||||
## Examples |
||||
|
||||
Here is one example for get you started: |
||||
|
||||
```python |
||||
from django.db import models |
||||
from graphene_django import DjangoObjectType |
||||
|
||||
class UserModel(models.Model): |
||||
name = models.CharField(max_length=100) |
||||
last_name = models.CharField(max_length=100) |
||||
|
||||
class User(DjangoObjectType): |
||||
class Meta: |
||||
# This type will transform all the UserModel fields |
||||
# into Graphene fields automatically |
||||
model = UserModel |
||||
|
||||
# An extra field in the User Type |
||||
full_name = graphene.String() |
||||
|
||||
def resolve_full_name(self, args, context, info): |
||||
return "{} {}".format(self.name, self.last_name) |
||||
``` |
||||
|
||||
If you want to learn even more, you can also check the following [examples](examples/): |
||||
|
||||
* **Schema with Filtering**: [Cookbook example](examples/cookbook) |
||||
* **Relay Schema**: [Starwars Relay example](examples/starwars) |
||||
|
||||
|
||||
## Contributing |
||||
|
||||
After cloning this repo, ensure dependencies are installed by running: |
||||
|
||||
```sh |
||||
python setup.py install |
||||
``` |
||||
|
||||
After developing, the full test suite can be evaluated by running: |
||||
|
||||
```sh |
||||
python setup.py test # Use --pytest-args="-v -s" for verbose mode |
||||
``` |
@ -0,0 +1,7 @@
|
||||
#!/bin/bash |
||||
|
||||
# Install the required scripts with |
||||
# pip install autoflake autopep8 isort |
||||
autoflake ./examples/ ./graphene_django/ -r --remove-unused-variables --remove-all-unused-imports --in-place |
||||
autopep8 ./examples/ ./graphene_django/ -r --in-place --experimental --aggressive --max-line-length 120 |
||||
isort -rc ./examples/ ./graphene_django/ |
@ -0,0 +1,3 @@
|
||||
#!/bin/bash |
||||
|
||||
pandoc README.md --from markdown --to rst -s -o README.rst |
@ -0,0 +1,18 @@
|
||||
import sys, os |
||||
ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) |
||||
sys.path.insert(0, ROOT_PATH + '/examples/') |
||||
|
||||
SECRET_KEY = 1 |
||||
|
||||
INSTALLED_APPS = [ |
||||
'graphene_django', |
||||
'graphene_django.tests', |
||||
'starwars', |
||||
] |
||||
|
||||
DATABASES = { |
||||
'default': { |
||||
'ENGINE': 'django.db.backends.sqlite3', |
||||
'NAME': 'django_test.sqlite', |
||||
} |
||||
} |
@ -0,0 +1,64 @@
|
||||
Cookbook Example Django Project |
||||
=============================== |
||||
|
||||
This example project demos integration between Graphene and Django. |
||||
The project contains two apps, one named `ingredients` and another |
||||
named `recepies`. |
||||
|
||||
Getting started |
||||
--------------- |
||||
|
||||
First you'll need to get the source of the project. Do this by cloning the |
||||
whole Graphene repository: |
||||
|
||||
```bash |
||||
# Get the example project code |
||||
git clone https://github.com/graphql-python/graphene.git |
||||
cd graphene/examples/cookbook |
||||
``` |
||||
|
||||
It is good idea (but not required) to create a virtual environment |
||||
for this project. We'll do this using |
||||
[virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) |
||||
to keep things simple, |
||||
but you may also find something like |
||||
[virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) |
||||
to be useful: |
||||
|
||||
```bash |
||||
# Create a virtualenv in which we can install the dependencies |
||||
virtualenv env |
||||
source env/bin/activate |
||||
``` |
||||
|
||||
Now we can install our dependencies: |
||||
|
||||
```bash |
||||
pip install -r requirements.txt |
||||
``` |
||||
|
||||
Now setup our database: |
||||
|
||||
```bash |
||||
# Setup the database |
||||
./manage.py migrate |
||||
|
||||
# Load some example data |
||||
./manage.py loaddata ingredients |
||||
|
||||
# Create an admin user (useful for logging into the admin UI |
||||
# at http://127.0.0.1:8000/admin) |
||||
./manage.py createsuperuser |
||||
``` |
||||
|
||||
Now you should be ready to start the server: |
||||
|
||||
```bash |
||||
./manage.py runserver |
||||
``` |
||||
|
||||
Now head on over to |
||||
[http://127.0.0.1:8000/graphiql](http://127.0.0.1:8000/graphiql) |
||||
and run some queries! |
||||
(See the [Django quickstart guide](http://graphene-python.org/docs/quickstart-django/) |
||||
for some example queries) |
@ -0,0 +1,6 @@
|
||||
from django.contrib import admin |
||||
|
||||
from cookbook.ingredients.models import Category, Ingredient |
||||
|
||||
admin.site.register(Ingredient) |
||||
admin.site.register(Category) |
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig |
||||
|
||||
|
||||
class IngredientsConfig(AppConfig): |
||||
name = 'cookbook.ingredients' |
||||
label = 'ingredients' |
||||
verbose_name = 'Ingredients' |
@ -0,0 +1 @@
|
||||
[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}] |
@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*- |
||||
# Generated by Django 1.9 on 2015-12-04 18:15 |
||||
from __future__ import unicode_literals |
||||
|
||||
import django.db.models.deletion |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
initial = True |
||||
|
||||
dependencies = [ |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='Category', |
||||
fields=[ |
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('name', models.CharField(max_length=100)), |
||||
], |
||||
), |
||||
migrations.CreateModel( |
||||
name='Ingredient', |
||||
fields=[ |
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('name', models.CharField(max_length=100)), |
||||
('notes', models.TextField()), |
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), |
||||
], |
||||
), |
||||
] |
@ -0,0 +1,17 @@
|
||||
from django.db import models |
||||
|
||||
|
||||
class Category(models.Model): |
||||
name = models.CharField(max_length=100) |
||||
|
||||
def __str__(self): |
||||
return self.name |
||||
|
||||
|
||||
class Ingredient(models.Model): |
||||
name = models.CharField(max_length=100) |
||||
notes = models.TextField() |
||||
category = models.ForeignKey(Category, related_name='ingredients') |
||||
|
||||
def __str__(self): |
||||
return self.name |
@ -0,0 +1,38 @@
|
||||
from cookbook.ingredients.models import Category, Ingredient |
||||
from graphene import ObjectType, Field, AbstractType, Node |
||||
from graphene_django.filter import DjangoFilterConnectionField |
||||
from graphene_django.types import DjangoObjectType |
||||
|
||||
|
||||
# Graphene will automatically map the Category model's fields onto the CategoryNode. |
||||
# This is configured in the CategoryNode's Meta class (as you can see below) |
||||
class CategoryNode(DjangoObjectType): |
||||
|
||||
class Meta: |
||||
model = Category |
||||
interfaces = (Node, ) |
||||
filter_fields = ['name', 'ingredients'] |
||||
filter_order_by = ['name'] |
||||
|
||||
|
||||
class IngredientNode(DjangoObjectType): |
||||
|
||||
class Meta: |
||||
model = Ingredient |
||||
# Allow for some more advanced filtering here |
||||
interfaces = (Node, ) |
||||
filter_fields = { |
||||
'name': ['exact', 'icontains', 'istartswith'], |
||||
'notes': ['exact', 'icontains'], |
||||
'category': ['exact'], |
||||
'category__name': ['exact'], |
||||
} |
||||
filter_order_by = ['name', 'category__name'] |
||||
|
||||
|
||||
class Query(AbstractType): |
||||
category = Field(CategoryNode) |
||||
all_categories = DjangoFilterConnectionField(CategoryNode) |
||||
|
||||
ingredient = Field(IngredientNode) |
||||
all_ingredients = DjangoFilterConnectionField(IngredientNode) |
@ -0,0 +1,2 @@
|
||||
|
||||
# Create your tests here. |
@ -0,0 +1,2 @@
|
||||
|
||||
# Create your views here. |
@ -0,0 +1,6 @@
|
||||
from django.contrib import admin |
||||
|
||||
from cookbook.recipes.models import Recipe, RecipeIngredient |
||||
|
||||
admin.site.register(Recipe) |
||||
admin.site.register(RecipeIngredient) |
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig |
||||
|
||||
|
||||
class RecipesConfig(AppConfig): |
||||
name = 'cookbook.recipes' |
||||
label = 'recipes' |
||||
verbose_name = 'Recipes' |
@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*- |
||||
# Generated by Django 1.9 on 2015-12-04 18:20 |
||||
from __future__ import unicode_literals |
||||
|
||||
import django.db.models.deletion |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
initial = True |
||||
|
||||
dependencies = [ |
||||
('ingredients', '0001_initial'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='Recipe', |
||||
fields=[ |
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('title', models.CharField(max_length=100)), |
||||
('instructions', models.TextField()), |
||||
], |
||||
), |
||||
migrations.CreateModel( |
||||
name='RecipeIngredient', |
||||
fields=[ |
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('amount', models.FloatField()), |
||||
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), |
||||
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), |
||||
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), |
||||
], |
||||
), |
||||
] |
@ -0,0 +1,19 @@
|
||||
from django.db import models |
||||
|
||||
from cookbook.ingredients.models import Ingredient |
||||
|
||||
|
||||
class Recipe(models.Model): |
||||
title = models.CharField(max_length=100) |
||||
instructions = models.TextField() |
||||
|
||||
|
||||
class RecipeIngredient(models.Model): |
||||
recipes = models.ForeignKey(Recipe, related_name='amounts') |
||||
ingredient = models.ForeignKey(Ingredient, related_name='used_by') |
||||
amount = models.FloatField() |
||||
unit = models.CharField(max_length=20, choices=( |
||||
('kg', 'Kilograms'), |
||||
('l', 'Litres'), |
||||
('', 'Units'), |
||||
)) |
@ -0,0 +1,2 @@
|
||||
|
||||
# Create your tests here. |
@ -0,0 +1,2 @@
|
||||
|
||||
# Create your views here. |
@ -0,0 +1,9 @@
|
||||
import graphene |
||||
import cookbook.ingredients.schema |
||||
|
||||
# print cookbook.ingredients.schema.Query._meta.graphql_type.get_fields()['allIngredients'].args |
||||
|
||||
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType): |
||||
pass |
||||
|
||||
schema = graphene.Schema(query=Query) |
@ -0,0 +1,125 @@
|
||||
""" |
||||
Django settings for cookbook project. |
||||
|
||||
Generated by 'django-admin startproject' using Django 1.9. |
||||
|
||||
For more information on this file, see |
||||
https://docs.djangoproject.com/en/1.9/topics/settings/ |
||||
|
||||
For the full list of settings and their values, see |
||||
https://docs.djangoproject.com/en/1.9/ref/settings/ |
||||
""" |
||||
|
||||
import os |
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) |
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production |
||||
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ |
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret! |
||||
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4' |
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production! |
||||
DEBUG = True |
||||
|
||||
ALLOWED_HOSTS = [] |
||||
|
||||
|
||||
# Application definition |
||||
|
||||
INSTALLED_APPS = [ |
||||
'django.contrib.admin', |
||||
'django.contrib.auth', |
||||
'django.contrib.contenttypes', |
||||
'django.contrib.sessions', |
||||
'django.contrib.messages', |
||||
'django.contrib.staticfiles', |
||||
'django_graphiql', |
||||
|
||||
'cookbook.ingredients.apps.IngredientsConfig', |
||||
'cookbook.recipes.apps.RecipesConfig', |
||||
] |
||||
|
||||
MIDDLEWARE_CLASSES = [ |
||||
'django.middleware.security.SecurityMiddleware', |
||||
'django.contrib.sessions.middleware.SessionMiddleware', |
||||
'django.middleware.common.CommonMiddleware', |
||||
'django.middleware.csrf.CsrfViewMiddleware', |
||||
'django.contrib.auth.middleware.AuthenticationMiddleware', |
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', |
||||
'django.contrib.messages.middleware.MessageMiddleware', |
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware', |
||||
] |
||||
|
||||
ROOT_URLCONF = 'cookbook.urls' |
||||
|
||||
TEMPLATES = [ |
||||
{ |
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates', |
||||
'DIRS': [], |
||||
'APP_DIRS': True, |
||||
'OPTIONS': { |
||||
'context_processors': [ |
||||
'django.template.context_processors.debug', |
||||
'django.template.context_processors.request', |
||||
'django.contrib.auth.context_processors.auth', |
||||
'django.contrib.messages.context_processors.messages', |
||||
], |
||||
}, |
||||
}, |
||||
] |
||||
|
||||
WSGI_APPLICATION = 'cookbook.wsgi.application' |
||||
|
||||
|
||||
# Database |
||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases |
||||
|
||||
DATABASES = { |
||||
'default': { |
||||
'ENGINE': 'django.db.backends.sqlite3', |
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), |
||||
} |
||||
} |
||||
|
||||
|
||||
# Password validation |
||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators |
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [ |
||||
{ |
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', |
||||
}, |
||||
{ |
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', |
||||
}, |
||||
{ |
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', |
||||
}, |
||||
{ |
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', |
||||
}, |
||||
] |
||||
|
||||
|
||||
# Internationalization |
||||
# https://docs.djangoproject.com/en/1.9/topics/i18n/ |
||||
|
||||
LANGUAGE_CODE = 'en-us' |
||||
|
||||
TIME_ZONE = 'UTC' |
||||
|
||||
USE_I18N = True |
||||
|
||||
USE_L10N = True |
||||
|
||||
USE_TZ = True |
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images) |
||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/ |
||||
|
||||
STATIC_URL = '/static/' |
@ -0,0 +1,12 @@
|
||||
from django.conf.urls import include, url |
||||
from django.contrib import admin |
||||
from django.views.decorators.csrf import csrf_exempt |
||||
|
||||
from cookbook.schema import schema |
||||
from graphene_django.views import GraphQLView |
||||
|
||||
urlpatterns = [ |
||||
url(r'^admin/', admin.site.urls), |
||||
url(r'^graphql', csrf_exempt(GraphQLView.as_view(schema=schema))), |
||||
url(r'^graphiql', include('django_graphiql.urls')), |
||||
] |
@ -0,0 +1,16 @@
|
||||
""" |
||||
WSGI config for cookbook project. |
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``. |
||||
|
||||
For more information on this file, see |
||||
https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ |
||||
""" |
||||
|
||||
import os |
||||
|
||||
from django.core.wsgi import get_wsgi_application |
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") |
||||
|
||||
application = get_wsgi_application() |
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python |
||||
import os |
||||
import sys |
||||
|
||||
if __name__ == "__main__": |
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") |
||||
|
||||
from django.core.management import execute_from_command_line |
||||
|
||||
execute_from_command_line(sys.argv) |
@ -0,0 +1,5 @@
|
||||
graphene[django] |
||||
django_graphiql |
||||
graphql-core |
||||
django==1.9 |
||||
django-filter==0.11.0 |
@ -0,0 +1,114 @@
|
||||
from .models import Character, Faction, Ship |
||||
|
||||
|
||||
def initialize(): |
||||
human = Character( |
||||
name='Human' |
||||
) |
||||
human.save() |
||||
|
||||
droid = Character( |
||||
name='Droid' |
||||
) |
||||
droid.save() |
||||
|
||||
rebels = Faction( |
||||
id='1', |
||||
name='Alliance to Restore the Republic', |
||||
hero=human |
||||
) |
||||
rebels.save() |
||||
|
||||
empire = Faction( |
||||
id='2', |
||||
name='Galactic Empire', |
||||
hero=droid |
||||
) |
||||
empire.save() |
||||
|
||||
xwing = Ship( |
||||
id='1', |
||||
name='X-Wing', |
||||
faction=rebels, |
||||
) |
||||
xwing.save() |
||||
|
||||
ywing = Ship( |
||||
id='2', |
||||
name='Y-Wing', |
||||
faction=rebels, |
||||
) |
||||
ywing.save() |
||||
|
||||
awing = Ship( |
||||
id='3', |
||||
name='A-Wing', |
||||
faction=rebels, |
||||
) |
||||
awing.save() |
||||
|
||||
# Yeah, technically it's Corellian. But it flew in the service of the rebels, |
||||
# so for the purposes of this demo it's a rebel ship. |
||||
falcon = Ship( |
||||
id='4', |
||||
name='Millenium Falcon', |
||||
faction=rebels, |
||||
) |
||||
falcon.save() |
||||
|
||||
homeOne = Ship( |
||||
id='5', |
||||
name='Home One', |
||||
faction=rebels, |
||||
) |
||||
homeOne.save() |
||||
|
||||
tieFighter = Ship( |
||||
id='6', |
||||
name='TIE Fighter', |
||||
faction=empire, |
||||
) |
||||
tieFighter.save() |
||||
|
||||
tieInterceptor = Ship( |
||||
id='7', |
||||
name='TIE Interceptor', |
||||
faction=empire, |
||||
) |
||||
tieInterceptor.save() |
||||
|
||||
executor = Ship( |
||||
id='8', |
||||
name='Executor', |
||||
faction=empire, |
||||
) |
||||
executor.save() |
||||
|
||||
|
||||
def create_ship(ship_name, faction_id): |
||||
new_ship = Ship( |
||||
name=ship_name, |
||||
faction_id=faction_id |
||||
) |
||||
new_ship.save() |
||||
return new_ship |
||||
|
||||
|
||||
def get_ship(_id): |
||||
return Ship.objects.get(id=_id) |
||||
|
||||
|
||||
def get_ships(): |
||||
return Ship.objects.all() |
||||
|
||||
|
||||
def get_faction(_id): |
||||
return Faction.objects.get(id=_id) |
||||
|
||||
|
||||
def get_rebels(): |
||||
return get_faction(1) |
||||
|
||||
|
||||
def get_empire(): |
||||
return get_faction(2) |
@ -0,0 +1,26 @@
|
||||
from __future__ import absolute_import |
||||
|
||||
from django.db import models |
||||
|
||||
|
||||
class Character(models.Model): |
||||
name = models.CharField(max_length=50) |
||||
|
||||
def __str__(self): |
||||
return self.name |
||||
|
||||
|
||||
class Faction(models.Model): |
||||
name = models.CharField(max_length=50) |
||||
hero = models.ForeignKey(Character) |
||||
|
||||
def __str__(self): |
||||
return self.name |
||||
|
||||
|
||||
class Ship(models.Model): |
||||
name = models.CharField(max_length=50) |
||||
faction = models.ForeignKey(Faction, related_name='ships') |
||||
|
||||
def __str__(self): |
||||
return self.name |
@ -0,0 +1,87 @@
|
||||
import graphene |
||||
from graphene import relay, resolve_only_args, Schema |
||||
from graphene_django import DjangoObjectType |
||||
|
||||
from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship, |
||||
get_ships) |
||||
from .models import ( |
||||
Character as CharacterModel, |
||||
Faction as FactionModel, |
||||
Ship as ShipModel |
||||
) |
||||
|
||||
|
||||
class Ship(DjangoObjectType): |
||||
|
||||
class Meta: |
||||
model = ShipModel |
||||
interfaces = (relay.Node, ) |
||||
|
||||
@classmethod |
||||
def get_node(cls, id, context, info): |
||||
node = get_ship(id) |
||||
print(node) |
||||
return node |
||||
|
||||
|
||||
class Character(DjangoObjectType): |
||||
|
||||
class Meta: |
||||
model = CharacterModel |
||||
|
||||
|
||||
class Faction(DjangoObjectType): |
||||
|
||||
class Meta: |
||||
model = FactionModel |
||||
interfaces = (relay.Node, ) |
||||
|
||||
@classmethod |
||||
def get_node(cls, id, context, info): |
||||
return get_faction(id) |
||||
|
||||
|
||||
class IntroduceShip(relay.ClientIDMutation): |
||||
|
||||
class Input: |
||||
ship_name = graphene.String(required=True) |
||||
faction_id = graphene.String(required=True) |
||||
|
||||
ship = graphene.Field(Ship) |
||||
faction = graphene.Field(Faction) |
||||
|
||||
@classmethod |
||||
def mutate_and_get_payload(cls, input, context, info): |
||||
ship_name = input.get('ship_name') |
||||
faction_id = input.get('faction_id') |
||||
ship = create_ship(ship_name, faction_id) |
||||
faction = get_faction(faction_id) |
||||
return IntroduceShip(ship=ship, faction=faction) |
||||
|
||||
|
||||
class Query(graphene.ObjectType): |
||||
rebels = graphene.Field(Faction) |
||||
empire = graphene.Field(Faction) |
||||
node = relay.Node.Field() |
||||
ships = relay.ConnectionField(Ship, description='All the ships.') |
||||
|
||||
@resolve_only_args |
||||
def resolve_ships(self): |
||||
return get_ships() |
||||
|
||||
@resolve_only_args |
||||
def resolve_rebels(self): |
||||
return get_rebels() |
||||
|
||||
@resolve_only_args |
||||
def resolve_empire(self): |
||||
return get_empire() |
||||
|
||||
|
||||
class Mutation(graphene.ObjectType): |
||||
introduce_ship = IntroduceShip.Field() |
||||
|
||||
|
||||
# We register the Character Model because if not would be |
||||
# inaccessible for the schema |
||||
schema = Schema(query=Query, mutation=Mutation, types=[Ship, Character]) |
@ -0,0 +1,47 @@
|
||||
import pytest |
||||
|
||||
from ..data import initialize |
||||
from ..schema import schema |
||||
|
||||
pytestmark = pytest.mark.django_db |
||||
|
||||
|
||||
def test_correct_fetch_first_ship_rebels(): |
||||
initialize() |
||||
query = ''' |
||||
query RebelsShipsQuery { |
||||
rebels { |
||||
name, |
||||
hero { |
||||
name |
||||
} |
||||
ships(first: 1) { |
||||
edges { |
||||
node { |
||||
name |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
''' |
||||
expected = { |
||||
'rebels': { |
||||
'name': 'Alliance to Restore the Republic', |
||||
'hero': { |
||||
'name': 'Human' |
||||
}, |
||||
'ships': { |
||||
'edges': [ |
||||
{ |
||||
'node': { |
||||
'name': 'X-Wing' |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
} |
||||
result = schema.execute(query) |
||||
assert not result.errors |
||||
assert result.data == expected |
@ -0,0 +1,79 @@
|
||||
import pytest |
||||
|
||||
from ..data import initialize |
||||
from ..schema import schema |
||||
|
||||
pytestmark = pytest.mark.django_db |
||||
|
||||
|
||||
def test_mutations(): |
||||
initialize() |
||||
|
||||
query = ''' |
||||
mutation MyMutation { |
||||
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) { |
||||
ship { |
||||
id |
||||
name |
||||
} |
||||
faction { |
||||
name |
||||
ships { |
||||
edges { |
||||
node { |
||||
id |
||||
name |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
''' |
||||
expected = { |
||||
'introduceShip': { |
||||
'ship': { |
||||
'id': 'U2hpcDo5', |
||||
'name': 'Peter' |
||||
}, |
||||
'faction': { |
||||
'name': 'Alliance to Restore the Republic', |
||||
'ships': { |
||||
'edges': [{ |
||||
'node': { |
||||
'id': 'U2hpcDox', |
||||
'name': 'X-Wing' |
||||
} |
||||
}, { |
||||
'node': { |
||||
'id': 'U2hpcDoy', |
||||
'name': 'Y-Wing' |
||||
} |
||||
}, { |
||||
'node': { |
||||
'id': 'U2hpcDoz', |
||||
'name': 'A-Wing' |
||||
} |
||||
}, { |
||||
'node': { |
||||
'id': 'U2hpcDo0', |
||||
'name': 'Millenium Falcon' |
||||
} |
||||
}, { |
||||
'node': { |
||||
'id': 'U2hpcDo1', |
||||
'name': 'Home One' |
||||
} |
||||
}, { |
||||
'node': { |
||||
'id': 'U2hpcDo5', |
||||
'name': 'Peter' |
||||
} |
||||
}] |
||||
}, |
||||
} |
||||
} |
||||
} |
||||
result = schema.execute(query) |
||||
assert not result.errors |
||||
assert result.data == expected |
@ -0,0 +1,117 @@
|
||||
import pytest |
||||
|
||||
from ..data import initialize |
||||
from ..schema import schema |
||||
|
||||
pytestmark = pytest.mark.django_db |
||||
|
||||
|
||||
def test_correctly_fetches_id_name_rebels(): |
||||
initialize() |
||||
query = ''' |
||||
query RebelsQuery { |
||||
rebels { |
||||
id |
||||
name |
||||
} |
||||
} |
||||
''' |
||||
expected = { |
||||
'rebels': { |
||||
'id': 'RmFjdGlvbjox', |
||||
'name': 'Alliance to Restore the Republic' |
||||
} |
||||
} |
||||
result = schema.execute(query) |
||||
assert not result.errors |
||||
assert result.data == expected |
||||
|
||||
|
||||
def test_correctly_refetches_rebels(): |
||||
initialize() |
||||
query = ''' |
||||
query RebelsRefetchQuery { |
||||
node(id: "RmFjdGlvbjox") { |
||||
id |
||||
... on Faction { |
||||
name |
||||
} |
||||
} |
||||
} |
||||
''' |
||||
expected = { |
||||
'node': { |
||||
'id': 'RmFjdGlvbjox', |
||||
'name': 'Alliance to Restore the Republic' |
||||
} |
||||
} |
||||
result = schema.execute(query) |
||||
assert not result.errors |
||||
assert result.data == expected |
||||
|
||||
|
||||
def test_correctly_fetches_id_name_empire(): |
||||
initialize() |
||||
query = ''' |
||||
query EmpireQuery { |
||||
empire { |
||||
id |
||||
name |
||||
} |
||||
} |
||||
''' |
||||
expected = { |
||||
'empire': { |
||||
'id': 'RmFjdGlvbjoy', |
||||
'name': 'Galactic Empire' |
||||
} |
||||
} |
||||
result = schema.execute(query) |
||||
assert not result.errors |
||||
assert result.data == expected |
||||
|
||||
|
||||
def test_correctly_refetches_empire(): |
||||
initialize() |
||||
query = ''' |
||||
query EmpireRefetchQuery { |
||||
node(id: "RmFjdGlvbjoy") { |
||||
id |
||||
... on Faction { |
||||
name |
||||
} |
||||
} |
||||
} |
||||
''' |
||||
expected = { |
||||
'node': { |
||||
'id': 'RmFjdGlvbjoy', |
||||
'name': 'Galactic Empire' |
||||
} |
||||
} |
||||
result = schema.execute(query) |
||||
assert not result.errors |
||||
assert result.data == expected |
||||
|
||||
|
||||
def test_correctly_refetches_xwing(): |
||||
initialize() |
||||
query = ''' |
||||
query XWingRefetchQuery { |
||||
node(id: "U2hpcDox") { |
||||
id |
||||
... on Ship { |
||||
name |
||||
} |
||||
} |
||||
} |
||||
''' |
||||
expected = { |
||||
'node': { |
||||
'id': 'U2hpcDox', |
||||
'name': 'X-Wing' |
||||
} |
||||
} |
||||
result = schema.execute(query) |
||||
assert not result.errors |
||||
assert result.data == expected |
@ -0,0 +1,9 @@
|
||||
from .types import ( |
||||
DjangoObjectType, |
||||
) |
||||
from .fields import ( |
||||
DjangoConnectionField, |
||||
) |
||||
|
||||
__all__ = ['DjangoObjectType', |
||||
'DjangoConnectionField'] |
@ -0,0 +1,24 @@
|
||||
from django.db import models |
||||
|
||||
|
||||
class MissingType(object): |
||||
pass |
||||
|
||||
try: |
||||
UUIDField = models.UUIDField |
||||
except AttributeError: |
||||
# Improved compatibility for Django 1.6 |
||||
UUIDField = MissingType |
||||
|
||||
try: |
||||
from django.db.models.related import RelatedObject |
||||
except: |
||||
# Improved compatibility for Django 1.6 |
||||
RelatedObject = MissingType |
||||
|
||||
|
||||
try: |
||||
# Postgres fields are only available in Django 1.8+ |
||||
from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField, RangeField |
||||
except ImportError: |
||||
ArrayField, HStoreField, JSONField, RangeField = (MissingType, ) * 4 |
@ -0,0 +1,189 @@
|
||||
from django.db import models |
||||
from django.utils.encoding import force_text |
||||
|
||||
from graphene import Enum, List, ID, Boolean, Float, Int, String, Field, NonNull, Field, Dynamic |
||||
from graphene.types.json import JSONString |
||||
from graphene.types.datetime import DateTime |
||||
from graphene.utils.str_converters import to_const |
||||
from graphene.relay import is_node |
||||
|
||||
from .compat import (ArrayField, HStoreField, JSONField, RangeField, |
||||
RelatedObject, UUIDField) |
||||
from .utils import get_related_model, import_single_dispatch |
||||
from .fields import get_connection_field |
||||
|
||||
singledispatch = import_single_dispatch() |
||||
|
||||
|
||||
def convert_choice_name(name): |
||||
return to_const(force_text(name)) |
||||
|
||||
|
||||
def get_choices(choices): |
||||
for value, help_text in choices: |
||||
if isinstance(help_text, (tuple, list)): |
||||
for choice in get_choices(help_text): |
||||
yield choice |
||||
else: |
||||
name = convert_choice_name(help_text) |
||||
description = help_text |
||||
yield name, value, description |
||||
|
||||
|
||||
def convert_django_field_with_choices(field, registry=None): |
||||
choices = getattr(field, 'choices', None) |
||||
if choices: |
||||
meta = field.model._meta |
||||
name = '{}{}'.format(meta.object_name, field.name.capitalize()) |
||||
choices = list(get_choices(choices)) |
||||
named_choices = [(c[0], c[1]) for c in choices] |
||||
named_choices_descriptions = {c[0]:c[2] for c in choices} |
||||
|
||||
class EnumWithDescriptionsType(object): |
||||
@property |
||||
def description(self): |
||||
return named_choices_descriptions[self.name] |
||||
|
||||
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) |
||||
return enum(description=field.help_text) |
||||
return convert_django_field(field, registry) |
||||
|
||||
|
||||
@singledispatch |
||||
def convert_django_field(field, registry=None): |
||||
raise Exception( |
||||
"Don't know how to convert the Django field %s (%s)" % |
||||
(field, field.__class__)) |
||||
|
||||
|
||||
@convert_django_field.register(models.CharField) |
||||
@convert_django_field.register(models.TextField) |
||||
@convert_django_field.register(models.EmailField) |
||||
@convert_django_field.register(models.SlugField) |
||||
@convert_django_field.register(models.URLField) |
||||
@convert_django_field.register(models.GenericIPAddressField) |
||||
@convert_django_field.register(models.FileField) |
||||
@convert_django_field.register(UUIDField) |
||||
def convert_field_to_string(field, registry=None): |
||||
return String(description=field.help_text) |
||||
|
||||
|
||||
@convert_django_field.register(models.AutoField) |
||||
def convert_field_to_id(field, registry=None): |
||||
return ID(description=field.help_text) |
||||
|
||||
|
||||
@convert_django_field.register(models.PositiveIntegerField) |
||||
@convert_django_field.register(models.PositiveSmallIntegerField) |
||||
@convert_django_field.register(models.SmallIntegerField) |
||||
@convert_django_field.register(models.BigIntegerField) |
||||
@convert_django_field.register(models.IntegerField) |
||||
def convert_field_to_int(field, registry=None): |
||||
return Int(description=field.help_text) |
||||
|
||||
|
||||
@convert_django_field.register(models.BooleanField) |
||||
def convert_field_to_boolean(field, registry=None): |
||||
return NonNull(Boolean, description=field.help_text) |
||||
|
||||
|
||||
@convert_django_field.register(models.NullBooleanField) |
||||
def convert_field_to_nullboolean(field, registry=None): |
||||
return Boolean(description=field.help_text) |
||||
|
||||
|
||||
@convert_django_field.register(models.DecimalField) |
||||
@convert_django_field.register(models.FloatField) |
||||
def convert_field_to_float(field, registry=None): |
||||
return Float(description=field.help_text) |
||||
|
||||
|
||||
@convert_django_field.register(models.DateField) |
||||
def convert_date_to_string(field, registry=None): |
||||
return DateTime(description=field.help_text) |
||||
|
||||
|
||||
@convert_django_field.register(models.OneToOneRel) |
||||
def convert_onetoone_field_to_djangomodel(field, registry=None): |
||||
model = get_related_model(field) |
||||
|
||||
def dynamic_type(): |
||||
_type = registry.get_type_for_model(model) |
||||
if not _type: |
||||
return |
||||
|
||||
return Field(_type) |
||||
|
||||
return Dynamic(dynamic_type) |
||||
|
||||
|
||||
@convert_django_field.register(models.ManyToManyField) |
||||
@convert_django_field.register(models.ManyToManyRel) |
||||
@convert_django_field.register(models.ManyToOneRel) |
||||
def convert_field_to_list_or_connection(field, registry=None): |
||||
model = get_related_model(field) |
||||
|
||||
def dynamic_type(): |
||||
_type = registry.get_type_for_model(model) |
||||
if not _type: |
||||
return |
||||
|
||||
if is_node(_type): |
||||
return get_connection_field(_type) |
||||
return Field(List(_type)) |
||||
|
||||
return Dynamic(dynamic_type) |
||||
|
||||
|
||||
# For Django 1.6 |
||||
@convert_django_field.register(RelatedObject) |
||||
def convert_relatedfield_to_djangomodel(field, registry=None): |
||||
model = field.model |
||||
|
||||
def dynamic_type(): |
||||
_type = registry.get_type_for_model(model) |
||||
if not _type: |
||||
return |
||||
|
||||
if is_node(_type): |
||||
return get_connection_field(_type) |
||||
return Field(List(_type)) |
||||
|
||||
return Dynamic(dynamic_type) |
||||
|
||||
|
||||
@convert_django_field.register(models.OneToOneField) |
||||
@convert_django_field.register(models.ForeignKey) |
||||
def convert_field_to_djangomodel(field, registry=None): |
||||
model = get_related_model(field) |
||||
|
||||
def dynamic_type(): |
||||
_type = registry.get_type_for_model(model) |
||||
if not _type: |
||||
return |
||||
|
||||
return Field(_type, description=field.help_text) |
||||
|
||||
return Dynamic(dynamic_type) |
||||
|
||||
|
||||
@convert_django_field.register(ArrayField) |
||||
def convert_postgres_array_to_list(field, registry=None): |
||||
base_type = convert_django_field(field.base_field) |
||||
if not isinstance(base_type, (List, NonNull)): |
||||
base_type = type(base_type) |
||||
return List(base_type, description=field.help_text) |
||||
|
||||