Skip to content

Commit 4e1cd3e

Browse files
author
Rami Chowdhury
committed
Initial commit
0 parents  commit 4e1cd3e

File tree

15 files changed

+756
-0
lines changed

15 files changed

+756
-0
lines changed

.circleci/config.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: 2
2+
jobs:
3+
build:
4+
docker:
5+
- image: circleci/python:3.7
6+
steps:
7+
- checkout
8+
- run:
9+
name: Install Tox
10+
command: pip install tox
11+
- run:
12+
name: Tests with Tox
13+
command: tox
14+

.gitignore

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
pip-wheel-metadata/
24+
share/python-wheels/
25+
*.egg-info/
26+
.installed.cfg
27+
*.egg
28+
MANIFEST
29+
30+
# Installer logs
31+
pip-log.txt
32+
pip-delete-this-directory.txt
33+
34+
# Unit test / coverage reports
35+
htmlcov/
36+
.tox/
37+
.nox/
38+
.coverage
39+
.coverage.*
40+
.cache
41+
nosetests.xml
42+
coverage.xml
43+
*.cover
44+
.hypothesis/
45+
.pytest_cache/
46+
47+
# Sphinx documentation
48+
docs/_build/
49+
50+
# Jupyter Notebook
51+
.ipynb_checkpoints
52+
53+
# pyenv
54+
.python-version
55+
56+
# Environments
57+
.env
58+
.venv
59+
env/
60+
venv/
61+
ENV/
62+
env.bak/
63+
venv.bak/
64+
65+
# mkdocs documentation
66+
/site
67+
68+
# mypy
69+
.mypy_cache/
70+
.dmypy.json
71+
dmypy.json
72+
73+
# Pyre type checker
74+
.pyre/

.pre-commit-config.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
default_language_version:
2+
python: python3.7
3+
fail_fast: true
4+
repos:
5+
- repo: git://github.com/pre-commit/pre-commit-hooks
6+
rev: c8bad492e1b1d65d9126dba3fe3bd49a5a52b9d6 # v2.1.0
7+
hooks:
8+
- id: check-merge-conflict
9+
- id: check-yaml
10+
- id: debug-statements
11+
- id: end-of-file-fixer
12+
exclude: ^docs/.*$
13+
- id: trailing-whitespace
14+
exclude: README.md
15+
- repo: https://github.com/pre-commit/mirrors-mypy
16+
rev: v0.701
17+
hooks:
18+
- id: mypy
19+
language_version: python3.7
20+
args: [--ignore-missing-imports, --no-strict-optional]
21+
- repo: https://github.com/pre-commit/pre-commit-hooks
22+
rev: v2.0.0
23+
hooks:
24+
- id: flake8
25+
language_version: python3.7
26+
- repo: https://github.com/python/black
27+
rev: stable
28+
hooks:
29+
- id: black
30+
language_version: python3.7
31+

LICENSE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2019 Upside Travel
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# ![Graphene Logo](http://graphene-python.org/favicon.png) Graphene-Pydantic [![Build Status](https://travis-ci.org/graphql-python/graphene-pydantic.svg?branch=master)](https://circle-ci.org/graphql-python/graphene-sqlalchemy) [![PyPI version](https://badge.fury.io/py/graphene-pydantic.svg)](https://badge.fury.io/py/graphene-pydantic) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene-sqlalchemy/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene-sqlalchemy?branch=master)
2+
3+
4+
A [Pydantic](https://pydantic-docs.helpmanual.io/) integration for [Graphene](http://graphene-python.org/).
5+
6+
## Installation
7+
8+
```bash
9+
pip install "graphene-pydantic"
10+
```
11+
12+
## Examples
13+
14+
Here is a simple Pydantic model:
15+
16+
```python
17+
import pydantic
18+
19+
class PersonModel(pydantic.BaseModel):
20+
id: uuid.UUID
21+
first_name: str
22+
last_name: str
23+
24+
```
25+
26+
To create a GraphQL schema for it you simply have to write the following:
27+
28+
```python
29+
import graphene
30+
from graphene_pydantic import PydanticObjectType
31+
32+
class Person(PydanticObjectType):
33+
class Meta:
34+
model = PersonModel
35+
# only return specified fields
36+
only_fields = ("name",)
37+
# exclude specified fields
38+
exclude_fields = ("id",)
39+
40+
class Query(graphene.ObjectType):
41+
people = graphene.List(User)
42+
43+
def resolve_people(self, info):
44+
return get_people() # function returning `PersonModel`s
45+
46+
schema = graphene.Schema(query=Query)
47+
```
48+
49+
Then you can simply query the schema:
50+
51+
```python
52+
query = '''
53+
query {
54+
people {
55+
firstName,
56+
lastName
57+
}
58+
}
59+
'''
60+
result = schema.execute(query)
61+
```
62+
63+
64+
### Full Examples
65+
66+
Please see [the examples directory](./examples) for more.

graphene_pydantic/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .types import PydanticObjectType
2+
3+
__all__ = ["PydanticObjectType"]

graphene_pydantic/converters.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import typing as T
2+
import uuid
3+
import datetime
4+
import decimal
5+
import enum
6+
import inspect
7+
8+
from graphene import Field, Boolean, Dynamic, Enum, Float, Int, List, String, UUID, Union
9+
from graphene.types.base import BaseType
10+
try:
11+
from graphene.types.decimal import Decimal as GrapheneDecimal
12+
DECIMAL_SUPPORTED = True
13+
except ImportError:
14+
# graphene 2.1.5+ is required for Decimals
15+
DECIMAL_SUPPORTED = False
16+
17+
from graphene.types.datetime import Date, Time, DateTime
18+
from pydantic import fields
19+
20+
from .registry import Registry
21+
22+
23+
class ConversionError(TypeError):
24+
pass
25+
26+
27+
# Placeholder for NoneType, so that we can easily reference it later
28+
TYPE_NONE = type(None)
29+
30+
31+
def get_attr_resolver(attr_name: str) -> T.Callable:
32+
"""
33+
Return a helper function that resolves a field with the given name by
34+
looking it up as an attribute of the type we're trying to resolve it on.
35+
"""
36+
def _get_field(root, _info):
37+
return getattr(root, attr_name, None)
38+
return _get_field
39+
40+
41+
def convert_pydantic_field(field: fields.Field, registry: Registry, **field_kwargs) -> Field:
42+
"""
43+
Convert a Pydantic model field into a Graphene type field that we can add
44+
to the generated Graphene data model type.
45+
"""
46+
declared_type = getattr(field, "type_", None)
47+
field_kwargs.setdefault(
48+
"type", convert_pydantic_type(declared_type, field, registry)
49+
)
50+
field_kwargs.setdefault("required", field.required)
51+
field_kwargs.setdefault("default_value", field.default)
52+
# TODO: find a better way to get a field's description. Some ideas include:
53+
# - hunt down the description from the field's schema, or the schema
54+
# from the field's base model
55+
# - maybe even (Sphinx-style) parse attribute documentation
56+
field_kwargs.setdefault("description", field.__doc__)
57+
58+
return Field(resolver=get_attr_resolver(field.name), **field_kwargs)
59+
60+
61+
def to_graphene_type(type_: T.Type, field: fields.Field, registry: Registry = None) -> BaseType: # noqa: C901
62+
"""
63+
Map a native Python type to a Graphene-supported Field type, where possible.
64+
"""
65+
if type_ == uuid.UUID:
66+
return UUID
67+
elif type_ in (str, bytes):
68+
return String
69+
elif type_ == datetime.datetime:
70+
return DateTime
71+
elif type_ == datetime.date:
72+
return Date
73+
elif type_ == datetime.time:
74+
return Time
75+
elif type_ == bool:
76+
return Boolean
77+
elif type_ == float:
78+
return Float
79+
elif type_ == decimal.Decimal:
80+
return GrapheneDecimal if DECIMAL_SUPPORTED else Float
81+
elif type_ == int:
82+
return Int
83+
elif type_ in (tuple, list, set):
84+
# TODO: do Sets really belong here?
85+
return List
86+
elif hasattr(type_, '__origin__'):
87+
return convert_generic_type(type_, field, registry)
88+
elif issubclass(type_, enum.Enum):
89+
return Enum.from_enum(type_)
90+
elif registry and registry.get_type_for_model(type_):
91+
return registry.get_type_for_model(type_)
92+
elif inspect.isfunction(type_):
93+
# TODO: this may result in false positives?
94+
return Dynamic(type_)
95+
else:
96+
raise Exception(
97+
f"Don't know how to convert the Pydantic field {field!r} ({field.type_})"
98+
)
99+
100+
101+
def convert_pydantic_type(type_: T.Type, field: fields.Field, registry: Registry = None) -> BaseType: # noqa: C901
102+
"""
103+
Convert a Pydantic type to a Graphene Field type, including not just the
104+
native Python type but any additional metadata (e.g. shape) that Pydantic
105+
knows about.
106+
"""
107+
graphene_type = to_graphene_type(type_, field, registry)
108+
if field.shape == fields.Shape.SINGLETON:
109+
return graphene_type
110+
elif field.shape in (fields.Shape.LIST, fields.Shape.TUPLE, fields.Shape.SEQUENCE, fields.Shape.SET):
111+
# TODO: _should_ Sets remain here?
112+
return List(graphene_type)
113+
elif field.shape == fields.Shape.MAPPING:
114+
raise ConversionError(f"Don't know how to handle mappings in Graphene.")
115+
116+
117+
def convert_generic_type(type_, field, registry=None):
118+
"""
119+
Convert annotated Python generic types into the most appropriate Graphene
120+
Field type -- e.g. turn `typing.Union` into a Graphene Union.
121+
"""
122+
origin = type_.__origin__
123+
if not origin:
124+
raise ConversionError(f"Don't know how to convert type {type_!r} ({field})")
125+
# NOTE: This is a little clumsy, but working with generic types is; it's hard to
126+
# decide whether the origin type is a subtype of, say, T.Iterable since typical
127+
# Python functions like `isinstance()` don't work
128+
if origin == T.Union:
129+
return convert_union_type(type_, field, registry)
130+
elif origin in (T.Dict, T.OrderedDict, T.Mapping):
131+
raise ConversionError("Don't know how to handle mappings in Graphene")
132+
elif origin in (T.List, T.Set, T.Collection, T.Iterable):
133+
return List(to_graphene_type(type_, field, registry))
134+
else:
135+
raise ConversionError(f"Don't know how to handle {type_} (generic: {origin})")
136+
137+
138+
def convert_union_type(type_, field, registry=None):
139+
"""
140+
Convert an annotated Python Union type into a Graphene Union.
141+
"""
142+
wrapped_types = type_.__args__
143+
# NOTE: a typing.Optional decomposes to a Union[None, T], so we can return
144+
# the Graphene type for T; Pydantic will have already parsed it as optional
145+
if len(wrapped_types) == 2 and TYPE_NONE in wrapped_types:
146+
native_type = next(x for x in wrapped_types if x != TYPE_NONE)
147+
graphene_type = to_graphene_type(native_type, field, registry)
148+
return graphene_type
149+
else:
150+
# Otherwise, we use a little metaprogramming -- create our own unique
151+
# subclass of graphene.Union that knows its constituent Graphene types
152+
graphene_types = tuple(to_graphene_type(x, field, registry) for x in wrapped_types)
153+
internal_meta = type("Meta", (), {'types': graphene_types})
154+
155+
union_class_name = "".join(x.__name__ for x in wrapped_types)
156+
union_class = type(f"Union_{union_class_name}", (Union,), {'Meta': internal_meta})
157+
return union_class

graphene_pydantic/examples/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)