├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── test_pygraphy.py └── test_strawberry.py ├── docs ├── app.md ├── context.md ├── declaration.md ├── executor.md ├── index.md ├── introspection.md ├── static │ └── playground.jpg └── subscription.md ├── examples ├── __init__.py ├── complex_example.py ├── simple_example.py └── starwars │ ├── __init__.py │ └── schema.py ├── mkdocs.yml ├── pygraphy ├── __init__.py ├── context.py ├── encoder.py ├── exceptions.py ├── introspection.py ├── static │ └── playground.html ├── types │ ├── __init__.py │ ├── base.py │ ├── enum.py │ ├── field.py │ ├── input.py │ ├── interface.py │ ├── object.py │ ├── schema.py │ └── union.py ├── utils.py └── view.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── introspection_result ├── subscription_introspection ├── test_asyncio.py ├── test_definition.py ├── test_execution.py ├── test_introspection.py ├── test_recursive_def.py ├── test_utils.py ├── test_view.py └── test_websocket.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | node_modules/ 108 | package-lock.json 109 | .vscode/ 110 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | 4 | matrix: 5 | include: 6 | # This is a workaround to make 3.7 running in travis-ci 7 | # See https://github.com/travis-ci/travis-ci/issues/9815 8 | - python: 3.7 9 | script: tox 10 | dist: xenial 11 | sudo: true 12 | 13 | install: 14 | - pip install tox codecov 15 | 16 | script: 17 | - tox -e py 18 | 19 | after_success: 20 | - cd tests && codecov 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gwo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | release: build 2 | twine upload dist/* 3 | 4 | pre_release: build 5 | twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/* 6 | 7 | build: clean 8 | python setup.py bdist_wheel 9 | 10 | clean: test 11 | rm -vf dist/* 12 | rm -rvf build/* 13 | 14 | test: 15 | py.test tests 16 | 17 | doc: 18 | mkdocs build 19 | 20 | .PHONY: release 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pygraphy 2 | A modern pythonic GraphQL implementation, painless GraphQL developing experience for Pythonista. 3 | 4 | [![Build Status](https://travis-ci.org/ethe/pygraphy.svg?branch=master)](https://travis-ci.org/ethe/pygraphy) 5 | [![codecov](https://codecov.io/gh/ethe/pygraphy/branch/master/graph/badge.svg)](https://codecov.io/gh/ethe/pygraphy) 6 | [![pypi](https://badge.fury.io/py/pygraphy.svg)](https://pypi.org/project/pygraphy/) 7 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pygraphy.svg)](https://pypi.org/project/pygraphy/) 8 | 9 | 10 | ## Document 11 | 12 | See [official docs](https://pygraphy.org/). 13 | 14 | 15 | ## Quick Review 16 | All the behaviors of Pygraphy are no difference from your intuition. 17 | ```python 18 | import asyncio 19 | import pygraphy 20 | from typing import List, Optional 21 | from starlette.applications import Starlette 22 | import uvicorn 23 | 24 | 25 | app = Starlette(debug=True) 26 | 27 | 28 | class Episode(pygraphy.Enum): 29 | NEWHOPE = 4 30 | EMPIRE = 5 31 | JEDI = 6 32 | 33 | 34 | class Character(pygraphy.Interface): 35 | """ 36 | Character interface contains human and droid 37 | """ 38 | id: str 39 | name: str 40 | appears_in: List[Episode] 41 | 42 | @pygraphy.field 43 | def friends(self) -> Optional[List['Character']]: 44 | return [] 45 | 46 | 47 | class Human(pygraphy.Object, Character): 48 | """ 49 | Human object 50 | """ 51 | home_planet: str 52 | 53 | 54 | class Droid(pygraphy.Object, Character): 55 | """ 56 | Driod object 57 | """ 58 | primary_function: str 59 | 60 | 61 | class Query(pygraphy.Query): 62 | 63 | @pygraphy.field 64 | async def hero(self, episode: Episode) -> Optional[Character]: 65 | await asyncio.sleep(1) 66 | return Droid( 67 | id="2001", 68 | name="R2-D2", 69 | appears_in=[Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI], 70 | primary_function="Astromech", 71 | ) 72 | 73 | 74 | @app.route('/') 75 | class Schema(pygraphy.Schema): 76 | query: Optional[Query] 77 | 78 | 79 | if __name__ == '__main__': 80 | uvicorn.run(app, host='0.0.0.0', port=8000) 81 | 82 | ``` 83 | 84 | 85 | ## Installation 86 | 87 | ### Web Server Required 88 | `pip install 'pygraphy[web]'` 89 | 90 | ### Standalone Model and Query Handler 91 | `pip install 'pygraphy'` 92 | 93 | 94 | ## Features 95 | 96 | - Clean room Pythonic schema definition system 97 | - Model definition bases on Python Dataclass 98 | - Python Asyncio support 99 | - Context management bases on Python Context Variables 100 | - Introspection and GraphQL Playground support 101 | 102 | 103 | ## Comparation with GraphQL-Core(-Next) 104 | 105 | ### Advantages 106 | 107 | [GraphQL-Core-Next](https://github.com/graphql-python/graphql-core-next) is the official supporting implementation of GraphQL, and it is only a basic library. Generally, you should use Graphene or other wrapper libraries bases on it. Pygraphy is an integrated library that includes data mapping and model definition. 108 | 109 | GraphQL-Core-Next is directly translated from GraphQL.js, this leads to some weird behaviors such as [graphql-core-next/issues/37](https://github.com/graphql-python/graphql-core-next/issues/37#issuecomment-511633135), and it is too tough to make a wrapper for walking around. Pygraphy rewrites the schema definition system in a more pythonic way. By using Python Metaclass, Pygraphy supports class-style schema definition naturally. There is no more inharmony between lambda function resolver (ugly Js style) and instance method resolver. 110 | 111 | By using Context Variables which is added into Python in version 3.7, Pygraphy does not need to pass context through the call chain like graphql-core-next. 112 | 113 | Also, Pygraphy is faster than graphql-core-next, you can check benchmark results as below. 114 | 115 | And more, Pygraphy clearly support stateful subscription method with Python Asynchronous Generators, which is not elaborate in graphql-core-next. 116 | 117 | ### Disadvantages 118 | 119 | Pygraphy is still in pre-alpha version, and need stable, welcome feedback. 120 | 121 | Pygraphy **does not support** full features of GraphQL according to Spec right now, the rest part of Spec will be integrated literally in the future, it contains 122 | - Derectives 123 | - ID Scalar 124 | - Type Extensions 125 | - Some Validation Check 126 | 127 | Most of features are already implemented so do not panic. 128 | 129 | 130 | ## Benchmark 131 | 132 | Compare with Strawberry / graphql-core-next, Pygraphy is 4.5 times faster than it. 133 | 134 | ``` 135 | ↳ uname -a 136 | Darwin Workstation.local 19.0.0 Darwin Kernel Version 19.0.0: Thu Jul 11 18:37:36 PDT 2019; root:xnu-6153.0.59.141.4~1/RELEASE_X86_64 x86_64 137 | ↳ python -V 138 | Python 3.7.2 139 | ↳ time python benchmark/test_pygraphy.py 140 | python benchmark/test_pygraphy.py 3.48s user 0.10s system 99% cpu 3.596 total 141 | ↳ time python benchmark/test_strawberry.py 142 | python benchmark/test_strawberry.py 16.05s user 0.16s system 99% cpu 16.257 total 143 | ``` 144 | -------------------------------------------------------------------------------- /benchmark/test_pygraphy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pygraphy 3 | from typing import Optional 4 | 5 | 6 | class Patron(pygraphy.Object): 7 | id: str 8 | name: str 9 | age: int 10 | 11 | 12 | class Query(pygraphy.Query): 13 | 14 | @pygraphy.field 15 | async def patron(self) -> Patron: 16 | await asyncio.sleep(0) 17 | return Patron(id='1', name='Syrus', age=27) 18 | 19 | 20 | class Schema(pygraphy.Schema): 21 | query: Optional[Query] 22 | 23 | 24 | if __name__ == '__main__': 25 | loop = asyncio.get_event_loop() 26 | query = """ 27 | query something{ 28 | patron { 29 | id 30 | name 31 | age 32 | } 33 | } 34 | """ 35 | 36 | futures = [ 37 | asyncio.ensure_future( 38 | Schema.execute(query), loop=loop 39 | ) for _ in range(10000) 40 | ] 41 | gathered = asyncio.gather(*futures, loop=loop, return_exceptions=True) 42 | loop.run_until_complete(gathered) 43 | -------------------------------------------------------------------------------- /benchmark/test_strawberry.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from graphql import graphql 3 | import strawberry 4 | 5 | 6 | @strawberry.type 7 | class Patron: 8 | id: int 9 | name: str 10 | age: int 11 | 12 | 13 | @strawberry.type 14 | class Query: 15 | @strawberry.field 16 | async def patron(self, info) -> Patron: 17 | await asyncio.sleep(0) 18 | return Patron(id=1, name="Patrick", age=100) 19 | 20 | 21 | schema = strawberry.Schema(query=Query) 22 | 23 | if __name__ == '__main__': 24 | 25 | loop = asyncio.get_event_loop() 26 | query = """ 27 | query something{ 28 | patron { 29 | id 30 | name 31 | age 32 | } 33 | } 34 | """ 35 | 36 | futures = [ 37 | asyncio.ensure_future( 38 | graphql(schema, query), loop=loop 39 | ) for _ in range(10000) 40 | ] 41 | gathered = asyncio.gather(*futures, loop=loop, return_exceptions=True) 42 | loop.run_until_complete(gathered) 43 | -------------------------------------------------------------------------------- /docs/app.md: -------------------------------------------------------------------------------- 1 | Pygraphy integrates [Starlette](https://www.starlette.io) as a default web server. You can install web-integrated Pygraphy with `pip install pygraphy[web]`, or manually install the Starlette. Pygraphy checked the local environment every times you import it, if Pygraphy find that Starlette has already been installed, the Schema type would be replaced automatically to a Starlette View Schema type, it is a valid Starlette endpoint class and you can use it as normal. 2 | ```python 3 | import pygraphy 4 | from starlette.applications import Starlette 5 | import uvicorn 6 | 7 | 8 | app = Starlette(debug=True) 9 | 10 | 11 | # ... 12 | 13 | 14 | @app.route('/') 15 | class Schema(pygraphy.Schema): 16 | query: Optional[Query] 17 | 18 | 19 | if __name__ == '__main__': 20 | uvicorn.run(app, host='0.0.0.0', port=8000) 21 | ``` 22 | 23 | If you not installed Starlette, using Schema type as a Starlette endpoint would raise an exception. 24 | -------------------------------------------------------------------------------- /docs/context.md: -------------------------------------------------------------------------------- 1 | Pygraphy uses [Context Variables](https://docs.python.org/3/library/contextvars.html#module-contextvars) to manage the context of a query, which has been included in Python 3.7 and later. With context variables, you do not need to pass a context everywhere any more like graphql-core. 2 | ```python 3 | from pygraphy import context, Object, field 4 | 5 | 6 | class Schema(Object): 7 | 8 | @field 9 | def query_type(self): 10 | """ 11 | The type that query operations will be rooted at. 12 | """ 13 | schema = context.get().schema 14 | query_type = schema.__fields__['query'].ftype 15 | # Do whatever you want 16 | ``` 17 | 18 | The definition of context is posted blow: 19 | ```python 20 | @dataclasses.dataclass 21 | class Context: 22 | schema: 'Schema' 23 | root_ast: List[OperationDefinitionNode] 24 | request: Optional[Any] = None 25 | variables: Optional[Mapping[str, Any]] = None 26 | ``` 27 | 28 | Attributes: 29 | 30 | - schema: The Schema class which is processing now. 31 | - root_ast: The ast tree parsed from query string. 32 | - request: Request instance, passed into context from the argument of `Schema.execute`. 33 | - variables: Query variables. 34 | -------------------------------------------------------------------------------- /docs/declaration.md: -------------------------------------------------------------------------------- 1 | Inspired by [Strawberry](https://github.com/strawberry-graphql/strawberry), Pygraphy uses dataclass to define the schema. All type classes can be printed as a valid GraphQL SDL. Pygraphy would try to translate all fields name from snake case to camel case. 2 | 3 | ```python 4 | import pygraphy 5 | 6 | 7 | class Patron(pygraphy.Object): 8 | id: str 9 | name: str 10 | age: int 11 | 12 | 13 | class Query(pygraphy.Query): 14 | """ 15 | Query doc also can be printed 16 | """ 17 | 18 | @pygraphy.field 19 | async def patron(self) -> Patron: 20 | """ 21 | Return the patron 22 | """ 23 | return Patron(id='1', name='Gwo', age=25) 24 | 25 | @pygraphy.field 26 | async def exception(self, content: str) -> str: 27 | raise RuntimeError(content) 28 | 29 | ``` 30 | 31 | ## Object 32 | 33 | Declare Object schema by inheriting `pygraphy.Object`, Pygraphy checks the type signature by using [Python type annotation](https://docs.python.org/3/library/typing.html) and generate the SDL. Pygraphy supports adding the description to an Object class and its resolver fields. 34 | ```python 35 | from pygraphy import Object, field 36 | from typing import Optional 37 | 38 | 39 | class Node(Object): 40 | """ 41 | A node with at most one sub node. 42 | """ 43 | value: int 44 | description: str 45 | 46 | @field 47 | def sub_node(self, description: str = 'A node') -> Optional['Node']: 48 | """ 49 | The resolver of getting sub node. 50 | """ 51 | if self.value != 0: 52 | return Node(value-1, description=description) 53 | return None 54 | 55 | 56 | assert str(Node) == '''""" 57 | A node with at most one sub node. 58 | """ 59 | type Node { 60 | value: Int! 61 | description: String! 62 | "The resolver of getting sub node." 63 | subNode( 64 | description: String! = "A node" 65 | ): Node 66 | }''' 67 | ``` 68 | 69 | A class method marked with `pygraphy.field` decorator would be treated as a resolver field. 70 | 71 | ## Query 72 | 73 | Query type is a subclass of Object, and it implements two built-in resolver fields: `__schema` and `__type` to support GraphQL Introspection. 74 | ```python 75 | from pygraphy import Query as BaseQuery, field 76 | 77 | 78 | class Query(BaseQuery): 79 | """ 80 | Query object 81 | """ 82 | 83 | @field 84 | async def foo(self) -> int: 85 | """ 86 | Return an int 87 | """ 88 | return 1 89 | ``` 90 | 91 | Please note that Query class must not contain any non-resolver field. 92 | 93 | ## Schema 94 | 95 | A Schema class contains two non-resolver field: query and mutation. 96 | ```python 97 | from pygraphy import Schema as BaseSchema 98 | 99 | 100 | class Schema(pygraphy.Schema): 101 | query: Optional[Query] 102 | mutation: Optional[Mutation] 103 | ``` 104 | 105 | Both query and mutation field must be optional, because of following the GraphQL Spec, every root node of query should be allowed to return `null` when an error raising during the request. 106 | 107 | Types which are referenced with a Schema definition whether directly or indirectly would be all registered into Schema class, the SDL of those types will be generated with Schema class together. 108 | ```python 109 | import pygraphy 110 | from typing import List, Optional 111 | 112 | 113 | class Episode(pygraphy.Enum): 114 | NEWHOPE = 4 115 | EMPIRE = 5 116 | JEDI = 6 117 | 118 | 119 | class Character(pygraphy.Interface): 120 | """ 121 | Character interface contains human and droid 122 | """ 123 | id: str 124 | name: str 125 | appears_in: List[Episode] 126 | 127 | @pygraphy.field 128 | def friends(self) -> Optional[List['Character']]: 129 | return None 130 | 131 | 132 | class Human(pygraphy.Object, Character): 133 | """ 134 | Human object 135 | """ 136 | home_planet: str 137 | 138 | 139 | class Droid(pygraphy.Object, Character): 140 | """ 141 | Driod object 142 | """ 143 | primary_function: str 144 | 145 | 146 | class Query(pygraphy.Query): 147 | 148 | @pygraphy.field 149 | def hero(self, episode: Episode) -> Optional[Character]: 150 | return None 151 | 152 | @pygraphy.field 153 | def human(self, id: str = '1234') -> Optional[Human]: 154 | return Human( 155 | id=id, name='foo', appears_in=[Episode.NEWHOPE, Episode.EMPIRE], home_planet='Mars' 156 | ) 157 | 158 | @pygraphy.field 159 | def droid(self, id: str) -> Optional[Droid]: 160 | return None 161 | 162 | 163 | class Schema(pygraphy.Schema): 164 | query: Optional[Query] 165 | 166 | 167 | print(Schema) 168 | ''' 169 | Query() 170 | """ 171 | type Query { 172 | __schema: __Schema! 173 | __type( 174 | name: String! 175 | ): __Type 176 | droid( 177 | id: String! 178 | ): Droid 179 | hero( 180 | episode: Episode! 181 | ): Character 182 | human( 183 | id: String! = "1234" 184 | ): Human 185 | } 186 | 187 | (...other built-in types) 188 | 189 | """ 190 | Driod object 191 | """ 192 | type Droid implements Character { 193 | id: String! 194 | name: String! 195 | appearsIn: [Episode!]! 196 | primaryFunction: String! 197 | friends: [Character!] 198 | } 199 | 200 | """ 201 | An enumeration. 202 | """ 203 | enum Episode { 204 | NEWHOPE 205 | EMPIRE 206 | JEDI 207 | } 208 | 209 | """ 210 | Character interface contains human and droid 211 | """ 212 | interface Character { 213 | id: String! 214 | name: String! 215 | appearsIn: [Episode!]! 216 | friends: [Character!] 217 | } 218 | 219 | """ 220 | Human object 221 | """ 222 | type Human implements Character { 223 | id: String! 224 | name: String! 225 | appearsIn: [Episode!]! 226 | homePlanet: String! 227 | friends: [Character!] 228 | } 229 | 230 | """ 231 | Schema(query: Union[__main__.Query, NoneType]) 232 | """ 233 | schema { 234 | query: Query 235 | } 236 | ''' 237 | ``` 238 | 239 | ### Subscribable Schema 240 | 241 | Schema class does not support the subscription method of GraphQL, the Subscribable Schema and subscription will be introduced later. 242 | 243 | 244 | ## Enum 245 | 246 | Enum type is supported like a Python enum class. 247 | ```python 248 | from pygraphy import Enum 249 | 250 | 251 | class Episode(pygraphy.Enum): 252 | NEWHOPE = 4 253 | EMPIRE = 5 254 | JEDI = 6 255 | 256 | 257 | print(Episode) 258 | ''' 259 | """ 260 | An enumeration. 261 | """ 262 | enum Episode { 263 | NEWHOPE 264 | EMPIRE 265 | JEDI 266 | } 267 | ''' 268 | ``` 269 | 270 | ## Input 271 | 272 | The argument of resolver fields can be Input type. 273 | ```python 274 | class GeoInput(pygraphy.Input): 275 | lat: float 276 | lng: float 277 | 278 | @property 279 | def latlng(self): 280 | return "({},{})".format(self.lat, self.lng) 281 | 282 | 283 | class Address(pygraphy.Object): 284 | latlng: str 285 | 286 | 287 | class Query(pygraphy.Query): 288 | 289 | @pygraphy.field 290 | def address(self, geo: GeoInput) -> Address: 291 | return Address(latlng=geo.latlng) 292 | 293 | 294 | class Schema(pygraphy.Schema): 295 | query: Optional[Query] 296 | 297 | ``` 298 | 299 | Input type can not be the return type of a resolver. 300 | 301 | ## Interface 302 | 303 | An Interface type can be inherited to an Object type like a mixin class, all fields inside an Interface will be injected into each Object subclass. 304 | ```python 305 | from typing import Optional, List 306 | from pygraphy import Interface, Object, field 307 | 308 | 309 | class Character(Interface): 310 | """ 311 | Character interface contains human and droid 312 | """ 313 | id: str 314 | name: str 315 | 316 | @field 317 | def friends(self) -> Optional[List['Character']]: 318 | return None 319 | 320 | 321 | class Human(Object, Character): 322 | """ 323 | Human object 324 | """ 325 | home_planet: str 326 | 327 | 328 | class Droid(Object, Character): 329 | """ 330 | Droid object 331 | """ 332 | primary_function: str 333 | 334 | 335 | print(Human) 336 | ''' 337 | """ 338 | Human object 339 | """ 340 | type Human implements Character { 341 | id: String! 342 | name: String! 343 | homePlanet: String! 344 | friends: [Character!] 345 | } 346 | ''' 347 | ``` 348 | 349 | ## Union 350 | 351 | Union type also be supported in Pygraphy. 352 | ```python 353 | import pygraphy 354 | from typing import Optional, List 355 | 356 | 357 | class Foo(pygraphy.Object): 358 | a: str 359 | 360 | 361 | class Bar(pygraphy.Object): 362 | b: int 363 | 364 | 365 | class FooBar(pygraphy.Union): 366 | members = (Foo, Bar) 367 | 368 | 369 | class Query(pygraphy.Object): 370 | 371 | @pygraphy.field 372 | def get_foobar(self) -> List[FooBar]: 373 | return [Foo(a='test') for _ in range(5)] 374 | 375 | 376 | print(FooBar) 377 | ''' 378 | union FooBar = 379 | | Foo 380 | | Bar 381 | ''' 382 | ``` 383 | -------------------------------------------------------------------------------- /docs/executor.md: -------------------------------------------------------------------------------- 1 | Pygraphy can be used as a GraphQL schema declaration and query executor library, same with graphql-core. Call the `execute` method of a custom Schema and get the query response. 2 | 3 | ```python 4 | import asyncio 5 | import pygraphy 6 | from typing import Optional 7 | 8 | 9 | class Patron(pygraphy.Object): 10 | id: str 11 | name: str 12 | age: int 13 | 14 | 15 | class Query(pygraphy.Query): 16 | 17 | @pygraphy.field 18 | def patron(self) -> Patron: 19 | return Patron(id='1', name='Gwo', age=25) 20 | 21 | @pygraphy.field 22 | def exception(self, content: str) -> str: 23 | raise RuntimeError(content) 24 | 25 | 26 | class Schema(pygraphy.Schema): 27 | query: Optional[Query] 28 | 29 | 30 | query = """ 31 | query something { 32 | patron { 33 | id 34 | name 35 | age 36 | } 37 | } 38 | """ 39 | 40 | loop = asyncio.get_event_loop() 41 | result = loop.run_until_complete(Schema.execute(query)) 42 | assert result == { 43 | 'errors': None, 44 | 'data': {'patron': {'id': '1', 'name': 'Gwo', 'age': 25}} 45 | } 46 | ``` 47 | 48 | the `Schema.execute` method is defined like below: 49 | ```python 50 | async def execute( 51 | cls, 52 | query: str, 53 | variables: Optional[Dict[str, Any]] = None, 54 | request: Optional[Any] = None, 55 | serialize: bool = False 56 | ) 57 | ``` 58 | 59 | Parameters: 60 | 61 | - query: GraphQL query string. 62 | - variables: A dict of query variables, pass it if there are some variables in query string. 63 | - request: the request instance, it could be got from query context in resolver fields. It is useful if you want to get the request info in resolvers, such as HTTP headers. 64 | - serialize: If it is true, executor would return a JSON string which as already been dumped. Return a Python dict result as default. 65 | 66 | ## Asynchronous Executor 67 | 68 | Pygraphy fully supports `asyncio`, the Python native parallel model. Just define the resolver field as a coroutine function, Pygraphy would automatically executes it as a coroutine task. All resolver fields in a same Object would be executed parallel. 69 | 70 | ```python 71 | import asyncio 72 | import pygraphy 73 | from typing import Optional 74 | 75 | 76 | global_var = False 77 | 78 | 79 | class Query(pygraphy.Query): 80 | 81 | @pygraphy.field 82 | async def foo(self) -> bool: 83 | global global_var 84 | result = global_var 85 | await asyncio.sleep(0.1) 86 | global_var = True 87 | return result 88 | 89 | @pygraphy.field 90 | async def bar(self) -> bool: 91 | global global_var 92 | result = global_var 93 | await asyncio.sleep(0.1) 94 | global_var = True 95 | return result 96 | 97 | 98 | class Schema(pygraphy.Schema): 99 | query: Optional[Query] 100 | 101 | 102 | query = """ 103 | query test { 104 | foo 105 | bar 106 | } 107 | """ 108 | 109 | 110 | loop = asyncio.get_event_loop() 111 | result = loop.run_until_complete(Schema.execute(query)) 112 | # Obviously, foo and bar both return False, cause they are executed parallel. 113 | assert result == { 114 | 'errors': None, 115 | 'data': {'foo': False, 'bar': False} 116 | } 117 | 118 | ``` 119 | 120 | **Attention:** do not mix asynchronous resolvers and non-asynchronous resolvers together. the non-asynchronous resolvers would block the query process, it is a design of Python `asyncio`. 121 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Pygraphy 2 | 3 | *A modern and pythonic approach of GraphQL, painless GraphQL developing experience for Pythonista.* 4 | 5 | [![Build Status](https://travis-ci.org/ethe/pygraphy.svg?branch=master)](https://travis-ci.org/ethe/pygraphy) 6 | [![codecov](https://codecov.io/gh/ethe/pygraphy/branch/master/graph/badge.svg)](https://codecov.io/gh/ethe/pygraphy) 7 | [![pypi](https://badge.fury.io/py/pygraphy.svg)](https://pypi.org/project/pygraphy/) 8 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pygraphy.svg)](https://pypi.org/project/pygraphy/) 9 | 10 | --- 11 | 12 | ## Introduction 13 | 14 | Pygraphy is another Python approach of GraphQL. Compare with Graphql-Core-(Next), Pygraphy is totally rewrite the GraphQL model declaration system in pythonic way, rather than copying GraphQL.js 1:1. Therefore, Pygraphy is able to provide native developing experience with Pythonic way. 15 | 16 | ## Quick Review 17 | 18 | All the behaviors of Pygraphy are no difference from your intuition. 19 | ```python 20 | import asyncio 21 | import pygraphy 22 | from typing import List, Optional 23 | from starlette.applications import Starlette 24 | import uvicorn 25 | 26 | 27 | app = Starlette(debug=True) 28 | 29 | 30 | class Episode(pygraphy.Enum): 31 | NEWHOPE = 4 32 | EMPIRE = 5 33 | JEDI = 6 34 | 35 | 36 | class Character(pygraphy.Interface): 37 | """ 38 | Character interface contains human and droid 39 | """ 40 | id: str 41 | name: str 42 | appears_in: List[Episode] 43 | 44 | @pygraphy.field 45 | def friends(self) -> Optional[List['Character']]: 46 | return [] 47 | 48 | 49 | class Human(pygraphy.Object, Character): 50 | """ 51 | Human object 52 | """ 53 | home_planet: str 54 | 55 | 56 | class Droid(pygraphy.Object, Character): 57 | """ 58 | Droid object 59 | """ 60 | primary_function: str 61 | 62 | 63 | class Query(pygraphy.Query): 64 | 65 | @pygraphy.field 66 | async def hero(self, episode: Episode) -> Optional[Character]: 67 | await asyncio.sleep(1) 68 | return Droid( 69 | id="2001", 70 | name="R2-D2", 71 | appears_in=[Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI], 72 | primary_function="Astromech", 73 | ) 74 | 75 | 76 | @app.route('/') 77 | class Schema(pygraphy.Schema): 78 | query: Optional[Query] 79 | 80 | 81 | if __name__ == '__main__': 82 | uvicorn.run(app, host='0.0.0.0', port=8000) 83 | 84 | ``` 85 | 86 | ## Requirements 87 | 88 | Python 3.7+ 89 | 90 | ## Installation 91 | 92 | Pygraphy supports two installation mode: 93 | 94 | 1. `pip install 'pygraphy[web]'` for users want to use Pygraphy with built-in web app together like quickstart. 95 | 1. `pip install 'pygraphy'` for users want to use bare Pygraphy executor and model declaration system. 96 | -------------------------------------------------------------------------------- /docs/introspection.md: -------------------------------------------------------------------------------- 1 | Pygraphy supports GraphQL introspection, and it has already integrated the [GraphQL Playground](https://github.com/prisma/graphql-playground). Try to run the web server which is posted in the quick review at [Introduction](/) and visit [http://0.0.0.0:8000](http://0.0.0.0:8000) by a browser, then you can enjoy the playground in developing. 2 | 3 | ![Playground](./static/playground.jpg) 4 | 5 | The schema of Introspection are totally wrote with Pygraphy itself: [pygraphy/introspection.py](https://github.com/ethe/pygraphy/blob/master/pygraphy/introspection.py), which proves that Pygraphy has a powerful schema declaration availability. 6 | 7 | ## Playground Settings 8 | 9 | Using the attribute of schema class `PLAYGROUND_SETTINGS` and customize playground settings. 10 | 11 | ```python 12 | class Schema(pygraphy.Schema): 13 | 14 | PLAYGROUND_SETTINGS = { 15 | "request.credentials": "same-page" 16 | } 17 | 18 | query: Optional[Query] 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/static/playground.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethe/pygraphy/fe0f632af0d44d1472a2d06c31758494f741c661/docs/static/playground.jpg -------------------------------------------------------------------------------- /docs/subscription.md: -------------------------------------------------------------------------------- 1 | The Schema type does not support subscription method, because subscription needs a stateful connection between client and server, subscribable schema needs a different way of query executing. You can use `SubscribableSchema` if you have to implement a subscription API. 2 | ```python 3 | import asyncio 4 | import pygraphy 5 | from starlette.applications import Starlette 6 | import uvicorn 7 | 8 | 9 | app = Starlette(debug=True) 10 | 11 | 12 | class Beat(pygraphy.Object): 13 | beat: int 14 | 15 | @pygraphy.field 16 | def foo(self, arg: int) -> int: 17 | return arg * self.beat 18 | 19 | 20 | class Subscription(pygraphy.Object): 21 | 22 | @pygraphy.field 23 | async def beat(self) -> Beat: 24 | start = 0 25 | for _ in range(10): 26 | await asyncio.sleep(0.1) 27 | yield Beat(beat=start) 28 | start += 1 29 | 30 | 31 | @app.websocket_route('/ws') 32 | class SubSchema(pygraphy.SubscribableSchema): 33 | subscription: Optional[Subscription] 34 | 35 | 36 | if __name__ == '__main__': 37 | uvicorn.run(app, host='0.0.0.0', port=8000) 38 | 39 | ``` 40 | 41 | Root fields of Subscription should be a resolver field, and it must be an [asynchronous generator](https://www.python.org/dev/peps/pep-0525/). 42 | ```python 43 | @pygraphy.field 44 | async def beat(self) -> Beat: 45 | start = 0 46 | for _ in range(10): 47 | await asyncio.sleep(0.1) 48 | yield Beat(beat=start) 49 | start += 1 50 | ``` 51 | 52 | Each returned of generator would be sent to client as a subscription result. 53 | 54 | ## Behaviors of Subscription 55 | 56 | The `SubscribableSchema` is a subclass of Starlette `WebsocketEndpoint` if you use default Starlette integration, and it uses Websocket to maintain the state between client and server. `SubscribableSchema` also supports query and mutation method. Once Websocket connection established, it can be used to multiple query and mutation request. However, one Websocket connection can be only used to single subscription request, if a connection is handling a subscription, it does not response other request any more. 57 | 58 | The connection will be closed if a subscription is canceled by server. If a client does not want to subscribe the existing subscription, closing the connection is fine. 59 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethe/pygraphy/fe0f632af0d44d1472a2d06c31758494f741c661/examples/__init__.py -------------------------------------------------------------------------------- /examples/complex_example.py: -------------------------------------------------------------------------------- 1 | import pygraphy 2 | from typing import Optional, List 3 | 4 | 5 | class Foo(pygraphy.Object): 6 | a: str 7 | 8 | 9 | class Bar(pygraphy.Object): 10 | b: int 11 | 12 | 13 | class FooBar(pygraphy.Union): 14 | members = (Foo, Bar) 15 | 16 | 17 | class GeoInput(pygraphy.Input): 18 | lat: float 19 | lng: float 20 | 21 | @property 22 | def latlng(self): 23 | return "({},{})".format(self.lat, self.lng) 24 | 25 | 26 | class Address(pygraphy.Object): 27 | latlng: str 28 | 29 | @pygraphy.field 30 | def foobar(self) -> List[FooBar]: 31 | return [Foo(a='test') for _ in range(5)] 32 | 33 | 34 | class Query(pygraphy.Query): 35 | 36 | @pygraphy.field 37 | def address(self, geo: GeoInput) -> Address: 38 | return Address(latlng=geo.latlng) 39 | 40 | 41 | class Mutation(pygraphy.Object): 42 | 43 | @pygraphy.field 44 | def create_address(self, geo: GeoInput) -> Address: 45 | return Address(latlng=geo.latlng) 46 | 47 | 48 | class Schema(pygraphy.Schema): 49 | query: Optional[Query] 50 | mutation: Optional[Mutation] 51 | -------------------------------------------------------------------------------- /examples/simple_example.py: -------------------------------------------------------------------------------- 1 | import pygraphy 2 | from typing import Optional, List 3 | 4 | 5 | class Patron(pygraphy.Object): 6 | id: str 7 | name: str 8 | age: int 9 | 10 | 11 | class Query(pygraphy.Query): 12 | 13 | @pygraphy.field 14 | def patron(self) -> Patron: 15 | return Patron(id='1', name='Syrus', age=27) 16 | 17 | @pygraphy.field 18 | def patrons(self, ids: List[int]) -> List[Patron]: 19 | return [Patron(id=str(i), name='Syrus', age=27) for i in ids] 20 | 21 | @pygraphy.field 22 | def exception(self, content: str) -> str: 23 | raise RuntimeError(content) 24 | 25 | 26 | class Schema(pygraphy.Schema): 27 | query: Optional[Query] 28 | -------------------------------------------------------------------------------- /examples/starwars/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethe/pygraphy/fe0f632af0d44d1472a2d06c31758494f741c661/examples/starwars/__init__.py -------------------------------------------------------------------------------- /examples/starwars/schema.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pygraphy 3 | from typing import List, Optional 4 | from starlette.applications import Starlette 5 | import uvicorn 6 | 7 | 8 | app = Starlette(debug=True) 9 | 10 | 11 | class Episode(pygraphy.Enum): 12 | NEWHOPE = 4 13 | EMPIRE = 5 14 | JEDI = 6 15 | 16 | 17 | class Character(pygraphy.Interface): 18 | """ 19 | Character interface contains human and droid 20 | """ 21 | id: str 22 | name: str 23 | appears_in: List[Episode] 24 | 25 | @pygraphy.field 26 | def friends(self) -> Optional[List['Character']]: 27 | return None 28 | 29 | 30 | class Human(pygraphy.Object, Character): 31 | """ 32 | Human object 33 | """ 34 | home_planet: str 35 | 36 | 37 | class Droid(pygraphy.Object, Character): 38 | """ 39 | Droid object 40 | """ 41 | primary_function: str 42 | 43 | 44 | class Query(pygraphy.Query): 45 | 46 | @pygraphy.field 47 | def hero(self, episode: Episode) -> Optional[Character]: 48 | return None 49 | 50 | @pygraphy.field 51 | def human(self, id: str = '1234') -> Optional[Human]: 52 | return Human( 53 | id=id, name='foo', appears_in=[Episode.NEWHOPE, Episode.EMPIRE], home_planet='Mars' 54 | ) 55 | 56 | @pygraphy.field 57 | def droid(self, id: str) -> Optional[Droid]: 58 | return None 59 | 60 | 61 | @app.route('/') 62 | class Schema(pygraphy.Schema): 63 | 64 | PLAYGROUND_SETTINGS = { 65 | "request.credentials": "omit" 66 | } 67 | 68 | query: Optional[Query] 69 | 70 | 71 | class Beat(pygraphy.Object): 72 | beat: int 73 | 74 | @pygraphy.field 75 | def foo(self, arg: int) -> int: 76 | return arg * self.beat 77 | 78 | 79 | class Subscription(pygraphy.Object): 80 | 81 | @pygraphy.field 82 | async def beat(self) -> Beat: 83 | start = 0 84 | for _ in range(10): 85 | await asyncio.sleep(0.1) 86 | yield Beat(beat=start) 87 | start += 1 88 | 89 | 90 | @app.websocket_route('/ws') 91 | class SubSchema(pygraphy.SubscribableSchema): 92 | subscription: Optional[Subscription] 93 | 94 | 95 | if __name__ == '__main__': 96 | uvicorn.run(app, host='0.0.0.0', port=8000) 97 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Pygraphy 2 | site_description: A modern pythonic GraphQL implementation, painless GraphQL developing experience for Pythonista. 3 | 4 | theme: 5 | name: 'material' 6 | 7 | repo_name: ethe/pygraphy 8 | repo_url: https://github.com/ethe/pygraphy 9 | edit_uri: "" 10 | 11 | nav: 12 | - Introduction: 'index.md' 13 | - Schema Declaration: 'declaration.md' 14 | - Query Executor: 'executor.md' 15 | - Built-in Web Integration: 'app.md' 16 | - Introspection and Playground: 'introspection.md' 17 | - Context: 'context.md' 18 | - Subscription: 'subscription.md' 19 | 20 | markdown_extensions: 21 | - markdown.extensions.codehilite: 22 | guess_lang: false 23 | -------------------------------------------------------------------------------- /pygraphy/__init__.py: -------------------------------------------------------------------------------- 1 | from .types import Interface, Object, Union, Enum, Input, field, context 2 | from .introspection import Query 3 | try: 4 | import starlette # noqa 5 | from .view import Schema, SubscribableSchema 6 | except ImportError: 7 | from .introspection import ( 8 | WithMetaSchema as Schema, 9 | WithMetaSubSchema as SubscribableSchema 10 | ) 11 | 12 | 13 | __version__ = '0.2.5' 14 | __all__ = [ 15 | 'Interface', 16 | 'Object', 17 | 'Schema', 18 | 'Union', 19 | 'Enum', 20 | 'Input', 21 | 'field', 22 | 'Query', 23 | 'context', 24 | 'SubscribableSchema' 25 | ] 26 | -------------------------------------------------------------------------------- /pygraphy/context.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import dataclasses 3 | from typing import Any, Optional, Mapping, List 4 | from graphql.language.ast import OperationDefinitionNode 5 | 6 | 7 | if typing.TYPE_CHECKING: 8 | from .types import Schema 9 | 10 | 11 | @dataclasses.dataclass 12 | class Context: 13 | schema: 'Schema' 14 | root_ast: List[OperationDefinitionNode] 15 | request: Optional[Any] = None 16 | variables: Optional[Mapping[str, Any]] = None 17 | -------------------------------------------------------------------------------- /pygraphy/encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pygraphy import types 3 | 4 | 5 | class GraphQLEncoder(json.JSONEncoder): 6 | 7 | def default(self, obj): 8 | if issubclass(type(obj), types.Enum): 9 | return str(obj).split('.')[-1] 10 | elif isinstance(obj, Exception): 11 | return { 12 | 'message': str(obj), 13 | 'locations': [{'line': obj.location[0], 'column': obj.location[1]}] if hasattr(obj, 'location') else None, 14 | 'path': obj.path if hasattr(obj, 'path') else None 15 | } 16 | return super().default(obj) 17 | -------------------------------------------------------------------------------- /pygraphy/exceptions.py: -------------------------------------------------------------------------------- 1 | class ValidationError(Exception): 2 | pass 3 | 4 | 5 | class RuntimeError(Exception): 6 | def __init__(self, message, node, path): 7 | super().__init__(message) 8 | self.location = node.loc.source.get_location(node.loc.start) 9 | self.path = path 10 | -------------------------------------------------------------------------------- /pygraphy/introspection.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | from typing import List, Optional 4 | from .types import ( 5 | Enum, 6 | Object, 7 | field, 8 | metafield, 9 | Schema as BaseSchema, 10 | SubscribableSchema as BaseSubscribableSchema, 11 | context, 12 | Interface, 13 | Union, 14 | Input, 15 | ResolverField 16 | ) 17 | from .types.base import print_type 18 | from .utils import ( 19 | meta, 20 | is_optional, 21 | is_list, 22 | is_union, 23 | to_camel_case 24 | ) 25 | 26 | 27 | @meta 28 | class DirectiveLocation(Enum): 29 | QUERY = 0 30 | MUTATION = 1 31 | SUBSCRIPTION = 2 32 | FIELD = 3 33 | FRAGMENT_DEFINITION = 4 34 | FRAGMENT_SPREAD = 5 35 | INLINE_FRAGMENT = 6 36 | SCHEMA = 7 37 | SCALAR = 8 38 | OBJECT = 9 39 | FIELD_DEFINITION = 10 40 | ARGUMENT_DEFINITION = 11 41 | INTERFACE = 12 42 | UNION = 13 43 | ENUM = 14 44 | ENUM_VALUE = 15 45 | INPUT_OBJECT = 16 46 | INPUT_FIELD_DEFINITION = 17 47 | 48 | 49 | @meta 50 | class Directive(Object): 51 | name: str 52 | description: Optional[str] 53 | locations: List[DirectiveLocation] 54 | args: List['InputValue'] 55 | 56 | 57 | @meta 58 | class TypeKind(Enum): 59 | SCALAR = 0 60 | OBJECT = 1 61 | INTERFACE = 2 62 | UNION = 3 63 | ENUM = 4 64 | INPUT_OBJECT = 5 65 | LIST = 6 66 | NON_NULL = 7 67 | 68 | 69 | @meta 70 | class EnumValue(Object): 71 | name: str 72 | description: Optional[str] 73 | is_deprecated: bool 74 | deprecation_reason: Optional[str] 75 | 76 | 77 | @meta 78 | class InputValue(Object): 79 | 80 | @field 81 | def name(self) -> str: 82 | return to_camel_case(self._name) 83 | 84 | @field 85 | def description(self) -> Optional[str]: 86 | """ 87 | Not support yet 88 | """ 89 | return None 90 | 91 | @field 92 | def type(self) -> 'Type': 93 | t = Type() 94 | t._type = self.param 95 | return t 96 | 97 | @field 98 | def default_value(self) -> Optional[str]: 99 | # Do not support default value of input type field 100 | if not hasattr(self, '_param'): 101 | return None 102 | default = self._param.default 103 | return json.dumps(default) if default != inspect._empty else None 104 | 105 | 106 | @meta 107 | class Field(Object): 108 | 109 | @field 110 | def name(self) -> str: 111 | return to_camel_case(self._field.name) 112 | 113 | @field 114 | def description(self) -> Optional[str]: 115 | return self._field.description 116 | 117 | @field 118 | def args(self) -> List[InputValue]: 119 | if not isinstance(self._field, ResolverField): 120 | return [] 121 | args = [] 122 | for name, param in self._field.params.items(): 123 | value = InputValue() 124 | value._name = name 125 | value.param = param 126 | value._param = self._field._params[name] 127 | args.append(value) 128 | return args 129 | 130 | @field 131 | def type(self) -> 'Type': 132 | t = Type() 133 | t._type = self._field.ftype 134 | return t 135 | 136 | @field 137 | def is_deprecated(self) -> bool: 138 | """ 139 | Not support yet 140 | """ 141 | return False 142 | 143 | @field 144 | def deprecation_reason(self) -> Optional[str]: 145 | """ 146 | Not support yet 147 | """ 148 | return None 149 | 150 | 151 | @meta 152 | class Type(Object): 153 | 154 | @field 155 | def name(self) -> Optional[str]: 156 | if not is_optional(self._type): 157 | return None 158 | if is_list(self.type): 159 | return None 160 | return print_type(self.type, nonnull=False) 161 | 162 | @field 163 | def kind(self) -> TypeKind: 164 | if not is_optional(self._type): 165 | return TypeKind.NON_NULL 166 | type = self._type.__args__[0] 167 | if is_list(self.type): 168 | return TypeKind.LIST 169 | if issubclass(type, (str, int, float, bool)): 170 | return TypeKind.SCALAR 171 | elif issubclass(type, Object): 172 | return TypeKind.OBJECT 173 | elif issubclass(type, Interface): 174 | return TypeKind.INTERFACE 175 | elif issubclass(type, Union): 176 | return TypeKind.UNION 177 | elif issubclass(type, Enum): 178 | return TypeKind.ENUM 179 | elif issubclass(type, Input): 180 | return TypeKind.INPUT_OBJECT 181 | 182 | @field 183 | def description(self) -> Optional[str]: 184 | return self.type.__description__ if hasattr(self.type, '__description__') else self.type.__doc__ 185 | 186 | @field 187 | def interfaces(self) -> Optional[List['Type']]: 188 | """ 189 | OBJECT only 190 | """ 191 | if not is_optional(self._type) and is_list(self._type): 192 | return None 193 | if issubclass(self.type, Object): 194 | interfaces = [] 195 | for base in self.type.__bases__: 196 | if issubclass(base, Interface): 197 | t = Type() 198 | t._type = Optional[base] 199 | interfaces.append(t) 200 | return interfaces 201 | return None 202 | 203 | @field 204 | def possible_types(self) -> Optional[List['Type']]: 205 | """ 206 | INTERFACE and UNION only 207 | """ 208 | if not is_optional(self._type) and is_list(self._type): 209 | return None 210 | if issubclass(self.type, Interface): 211 | types = [] 212 | for subclass in self.type.__subclasses__(): 213 | t = Type() 214 | t._type = Optional[subclass] 215 | types.append(t) 216 | return types 217 | if issubclass(self.type, Union): 218 | types = [] 219 | for member in list(self.type.members): 220 | t = Type() 221 | t._type = Optional[member] 222 | types.append(t) 223 | return types 224 | return None 225 | 226 | @field 227 | def input_fields(self) -> Optional[List[InputValue]]: 228 | """ 229 | INPUT_OBJECT only 230 | """ 231 | if not is_optional(self._type) and is_list(self._type): 232 | return None 233 | if issubclass(self.type, Input): 234 | values = [] 235 | for name, tfield in self.type.__fields__.items(): 236 | value = InputValue() 237 | value._name = name 238 | value.param = tfield.ftype 239 | values.append(value) 240 | return values 241 | return None 242 | 243 | @field 244 | def of_type(self) -> Optional['Type']: 245 | """ 246 | NON_NULL and LIST only 247 | """ 248 | if not is_optional(self._type): 249 | of = Type() 250 | of._type = Optional[self._type] 251 | return of 252 | 253 | if is_list(self._type.__args__[0]): 254 | of = Type() 255 | of._type = self._type.__args__[0].__args__[0] 256 | return of 257 | return None 258 | 259 | @field 260 | def enum_values(self, include_deprecated: Optional[bool] = False) -> Optional[List[EnumValue]]: 261 | """ 262 | ENUM only 263 | """ 264 | if not is_optional(self._type) and is_list(self._type): 265 | return None 266 | if issubclass(self.type, Enum): 267 | values = [] 268 | for attr in dir(self.type): 269 | if attr.startswith('_'): 270 | continue 271 | values.append(attr) 272 | return [EnumValue( 273 | name=i, 274 | description=None, 275 | is_deprecated=False, 276 | deprecation_reason=None 277 | ) for i in values] 278 | 279 | @field 280 | def fields(self, include_deprecated: Optional[bool] = False) -> Optional[List[Field]]: 281 | """ 282 | OBJECT and INTERFACE only 283 | """ 284 | if not is_optional(self._type) and is_list(self._type): 285 | return None 286 | if issubclass(self.type, Object) or issubclass(self.type, Interface): 287 | fields = [] 288 | for f in self.type.__fields__.values(): 289 | field = Field() 290 | field._field = f 291 | fields.append(field) 292 | return fields 293 | return None 294 | 295 | @property 296 | def type(self): 297 | if is_union(self._type): 298 | return self._type.__args__[0] 299 | else: 300 | return self._type 301 | 302 | 303 | @meta 304 | class Schema(Object): 305 | 306 | @field 307 | def directives(self) -> List[Directive]: 308 | return [] 309 | 310 | @field 311 | def query_type(self) -> Type: 312 | """ 313 | The type that query operations will be rooted at. 314 | """ 315 | schema = context.get().schema 316 | type = Type() 317 | type._type = schema.__fields__['query'].ftype 318 | return type 319 | 320 | @field 321 | def mutation_type(self) -> Optional[Type]: 322 | """ 323 | If this server supports mutation, the type that mutation operations will be rooted at. 324 | """ 325 | schema = context.get().schema 326 | if 'mutation' not in schema.__fields__: 327 | return None 328 | type = Type() 329 | type._type = schema.__fields__['mutation'].ftype 330 | return type 331 | 332 | @field 333 | def subscription_type(self) -> Optional[Type]: 334 | """ 335 | Not support yet 336 | """ 337 | return None 338 | 339 | @field 340 | def types(self) -> List[Type]: 341 | types = [] 342 | self.init_scalars(types) 343 | for t in context.get().schema.registered_type: 344 | type = Type() 345 | type._type = Optional[t] 346 | types.append(type) 347 | return types 348 | 349 | @staticmethod 350 | def init_scalars(types): 351 | scalars = [int, float, str, bool] 352 | for scalar in scalars: 353 | t = Type() 354 | t._type = Optional[scalar] 355 | types.append(t) 356 | return types 357 | 358 | 359 | class Query(Object): 360 | 361 | @metafield 362 | def _type(self, name: str) -> Optional[Type]: 363 | return None 364 | 365 | @metafield 366 | def _schema(self) -> Schema: 367 | return Schema() 368 | 369 | 370 | class WithMetaSchema(BaseSchema): 371 | query: Optional[Query] 372 | 373 | 374 | class WithMetaSubSchema(BaseSubscribableSchema): 375 | query: Optional[Query] 376 | -------------------------------------------------------------------------------- /pygraphy/static/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | GraphQL Playground 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 62 | 63 | 484 |
485 | 518 |
Loading 519 | GraphQL Playground 520 |
521 |
522 | 523 |
524 | 540 | 541 | 542 | -------------------------------------------------------------------------------- /pygraphy/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .union import Union, UnionType 2 | from .enum import Enum, EnumType 3 | from .input import Input, InputType 4 | from .interface import Interface, InterfaceType 5 | from .object import DefaultObject as Object, ObjectType 6 | from .schema import Schema, SchemaType, context, Socket, SubscribableSchema 7 | from .field import field, metafield, Field, ResolverField 8 | 9 | 10 | __all__ = [ 11 | 'Interface', 12 | 'Object', 13 | 'Schema', 14 | 'Union', 15 | 'Enum', 16 | 'Input', 17 | 'field', 18 | 'UnionType', 19 | 'InputType', 20 | 'InterfaceType', 21 | 'ObjectType', 22 | 'SchemaType', 23 | 'EnumType', 24 | 'Field', 25 | 'ResolverField', 26 | 'context', 27 | 'Socket', 28 | 'SubscribableSchema' 29 | ] 30 | -------------------------------------------------------------------------------- /pygraphy/types/base.py: -------------------------------------------------------------------------------- 1 | from graphql.language.ast import ( 2 | IntValueNode, 3 | FloatValueNode, 4 | BooleanValueNode, 5 | StringValueNode, 6 | NullValueNode, 7 | EnumValueNode, 8 | ListValueNode, 9 | ObjectValueNode, 10 | VariableNode, 11 | ) 12 | from pygraphy.utils import ( 13 | patch_indents, 14 | is_union, 15 | is_optional, 16 | is_list, 17 | to_snake_case, 18 | to_camel_case 19 | ) 20 | from pygraphy.exceptions import ValidationError 21 | from pygraphy import types 22 | 23 | 24 | class GraphQLType(type): 25 | 26 | def print_description(cls, indent=0): 27 | return patch_indents( 28 | f'"""\n{cls.__description__}\n"""\n' if cls.__description__ else '', # noqa 29 | indent=indent 30 | ) 31 | 32 | 33 | VALID_BASIC_TYPES = { 34 | str: 'String', 35 | int: 'Int', 36 | float: 'Float', 37 | bool: 'Boolean', 38 | } 39 | 40 | 41 | def print_type(gtype, nonnull=True, except_types=()): 42 | if isinstance(gtype, except_types): 43 | raise ValidationError(f'{gtype} is not a valid type') 44 | literal = None 45 | if is_union(gtype): 46 | if is_optional(gtype): 47 | return f'{print_type(gtype.__args__[0], nonnull=False, except_types=except_types)}' # noqa 48 | else: 49 | raise ValidationError( 50 | f'Native Union type is not supported except Optional' 51 | ) 52 | elif is_list(gtype): 53 | literal = f'[{print_type(gtype.__args__[0], except_types=except_types)}]' # noqa 54 | elif isinstance(gtype, types.ObjectType): 55 | literal = f'{gtype.__name__}' 56 | elif gtype in VALID_BASIC_TYPES: 57 | literal = VALID_BASIC_TYPES[gtype] 58 | elif gtype is None or gtype == type(None): # noqa 59 | return 'null' 60 | elif isinstance(gtype, types.UnionType): 61 | literal = f'{gtype.__name__}' 62 | elif isinstance(gtype, types.EnumType): 63 | literal = f'{gtype.__name__}' 64 | elif isinstance(gtype, types.InputType): 65 | literal = f'{gtype.__name__}' 66 | elif isinstance(gtype, types.InterfaceType): 67 | literal = f'{gtype.__name__}' 68 | else: 69 | raise ValidationError(f'Can not convert type {gtype} to GraphQL type') 70 | 71 | if nonnull: 72 | literal += '!' 73 | return literal 74 | 75 | 76 | def load_literal_value(node, ptype): 77 | if isinstance(node, IntValueNode): 78 | return int(node.value) 79 | elif isinstance(node, FloatValueNode): 80 | return float(node.value) 81 | elif isinstance(node, BooleanValueNode): 82 | return bool(node.value) 83 | elif isinstance(node, StringValueNode): 84 | return node.value 85 | elif isinstance(node, NullValueNode): 86 | return None 87 | elif isinstance(node, ListValueNode): 88 | return [load_literal_value(v, ptype.__args__[0]) for v in node.values] 89 | elif isinstance(node, EnumValueNode): 90 | value = getattr(ptype, node.value) 91 | if not value: 92 | raise RuntimeError( 93 | f'{node.value} is not a valid member of {type}', 94 | node 95 | ) 96 | return value 97 | elif isinstance(node, ObjectValueNode): 98 | if is_optional(ptype): 99 | return load_literal_value(node, ptype.__args__[0]) 100 | data = {} 101 | keys = ptype.__dataclass_fields__.keys() 102 | for field in node.fields: 103 | name = field.name.value 104 | if name not in keys: 105 | name = to_snake_case(name) 106 | data[name] = load_literal_value( 107 | field.value, ptype.__fields__[to_snake_case(name)].ftype) 108 | return ptype(**data) 109 | elif isinstance(node, VariableNode): 110 | name = node.name.value 111 | variables = types.context.get().variables 112 | if name not in variables: 113 | raise RuntimeError(f'Can not find variable {name}') 114 | variable = variables[name] 115 | return load_variable(variable, ptype) 116 | raise RuntimeError(f'Can not convert {node.value}', node) 117 | 118 | 119 | def load_variable(variable, ptype): 120 | if isinstance(ptype, types.InputType): 121 | data = {} 122 | keys = ptype.__dataclass_fields__.keys() 123 | for key, value in variable.items(): 124 | snake_cases = to_snake_case(key) 125 | data[snake_cases if key not in keys else key] = load_variable(value, ptype.__fields__[snake_cases].ftype) 126 | return ptype(**data) 127 | elif is_list(ptype): 128 | return [load_variable(i, ptype.__args__[0]) for i in variable] 129 | elif is_optional(ptype): 130 | if variable is None: 131 | return None 132 | return load_variable(variable, ptype.__args__[0]) 133 | elif isinstance(ptype, types.EnumType): 134 | return getattr(ptype, variable) 135 | else: 136 | return variable 137 | -------------------------------------------------------------------------------- /pygraphy/types/enum.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from enum import EnumMeta, Enum as PyEnum 3 | from pygraphy.utils import patch_indents 4 | 5 | 6 | class EnumType(EnumMeta): 7 | 8 | def __str__(cls): 9 | description = inspect.getdoc(cls) 10 | description_literal = f'"""\n{description}\n"""\n' if description else '' # noqa 11 | return ( 12 | description_literal 13 | + f'enum {cls.__name__} ' 14 | + '{\n' 15 | + f'{patch_indents(cls.print_enum_values(), indent=1)}' 16 | + '\n}' 17 | ) 18 | 19 | def print_enum_values(cls): 20 | literal = '' 21 | for name, _ in cls.__dict__.items(): 22 | if name.startswith('_'): 23 | continue 24 | literal += (name + '\n') 25 | return literal[:-1] if literal.endswith('\n') else literal 26 | 27 | 28 | class Enum(PyEnum, metaclass=EnumType): 29 | pass 30 | -------------------------------------------------------------------------------- /pygraphy/types/field.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import typing 4 | import inspect 5 | import dataclasses 6 | from typing import ( 7 | Mapping, 8 | Optional, 9 | _eval_type, 10 | ForwardRef, 11 | Union as PyUnion, 12 | List 13 | ) 14 | from pygraphy.utils import ( 15 | patch_indents, 16 | is_union, 17 | is_list, 18 | to_camel_case, 19 | to_snake_case 20 | ) 21 | from pygraphy.exceptions import ValidationError 22 | from .base import print_type, GraphQLType 23 | 24 | 25 | if typing.TYPE_CHECKING: 26 | from .object import Object 27 | 28 | 29 | def hidden(method): 30 | method.__hidden__ = True 31 | return method 32 | 33 | 34 | def metafield(method): 35 | method.__is_metafield__ = True 36 | return method 37 | 38 | 39 | def field(method): 40 | """ 41 | Mark class method as a resolver 42 | """ 43 | method.__is_field__ = True 44 | return method 45 | 46 | 47 | @dataclasses.dataclass 48 | class Field: 49 | _obj: 'Object' 50 | name: str 51 | _ftype: type 52 | description: Optional[str] 53 | 54 | def __str__(self): 55 | literal = f'{to_camel_case(self.name)}: {print_type(self.ftype)}' 56 | if self.description: 57 | literal = f'"{self.description}"\n' + literal 58 | return literal 59 | 60 | @property 61 | def ftype(self): 62 | return self.replace_forwarded_type(self._ftype) 63 | 64 | def replace_forwarded_type(self, ptype): 65 | if hasattr(ptype, '__args__'): 66 | args = [self.replace_forwarded_type(t) for t in ptype.__args__] 67 | if is_union(ptype): 68 | return PyUnion[tuple(args)] 69 | elif is_list(ptype): 70 | return List[tuple(args)] 71 | elif isinstance(ptype, (str, ForwardRef)): 72 | return self.get_type(ptype) 73 | return ptype 74 | 75 | def get_type(self, forwarded_type): 76 | actual_type = None 77 | for base in self._obj.__mro__: 78 | base_globals = sys.modules[base.__module__].__dict__ 79 | ref = forwarded_type 80 | if isinstance(forwarded_type, str): 81 | ref = ForwardRef(forwarded_type, is_argument=False) 82 | try: 83 | actual_type = _eval_type(ref, base_globals, None) 84 | break 85 | except NameError: 86 | continue 87 | if not actual_type: 88 | raise ValidationError(f'Can not find type {forwarded_type}') 89 | return actual_type 90 | 91 | 92 | @dataclasses.dataclass 93 | class ResolverField(Field): 94 | _params: Mapping[str, inspect.Parameter] 95 | 96 | @property 97 | def params(self): 98 | param_dict = {} 99 | for name, param in self._params.items(): 100 | param_dict[name] = self.replace_forwarded_type(param.annotation) 101 | return param_dict 102 | 103 | def __str__(self): 104 | if not self.params: 105 | literal = f'{to_camel_case(self.name)}: {print_type(self.ftype)}' 106 | else: 107 | literal = f'{to_camel_case(self.name)}' \ 108 | + '(\n' \ 109 | + patch_indents(self.print_args(), indent=1) \ 110 | + f'\n): {print_type(self.ftype)}' 111 | if self.description: 112 | literal = f'"{self.description}"\n' + literal 113 | return literal 114 | 115 | def print_args(self): 116 | literal = '' 117 | for name, param in self.params.items(): 118 | literal += f'{to_camel_case(name)}:' \ 119 | f' {print_type(param)}' \ 120 | f'{self.print_default_value(name)}\n' # noqa 121 | return literal[:-1] 122 | 123 | def print_default_value(self, name): 124 | default = self._params[name].default 125 | return f' = {json.dumps(default)}' if default != inspect._empty else '' 126 | 127 | 128 | class FieldableType(GraphQLType): 129 | 130 | def __new__(cls, name, bases, attrs): 131 | attrs['__fields__'] = {} 132 | attrs['__description__'] = None 133 | attrs['__validated__'] = False 134 | cls = dataclasses.dataclass(super().__new__(cls, name, bases, attrs)) 135 | sign = inspect.signature(cls) 136 | cls.__description__ = inspect.getdoc(cls) 137 | for name, t in sign.parameters.items(): 138 | cls.__fields__[to_snake_case(name)] = Field( 139 | name=to_snake_case(name), _ftype=t.annotation, description=None, _obj=cls 140 | ) 141 | return cls 142 | 143 | def print_field(cls, indent=0): 144 | literal = '' 145 | for _, field in cls.__fields__.items(): 146 | if field.name.startswith('__'): 147 | continue 148 | literal += f'{field}\n' 149 | return patch_indents(literal[:-1], indent) 150 | -------------------------------------------------------------------------------- /pygraphy/types/input.py: -------------------------------------------------------------------------------- 1 | from pygraphy.exceptions import ValidationError 2 | from pygraphy.utils import patch_indents 3 | from .base import print_type, load_literal_value 4 | from .object import ObjectType 5 | from .field import Field, FieldableType 6 | from .union import UnionType 7 | 8 | 9 | class InputType(FieldableType): 10 | 11 | def validate(cls): 12 | if cls.__validated__: 13 | return 14 | cls.__validated__ = True 15 | for _, field in cls.__fields__.items(): 16 | if not isinstance(field, Field): 17 | raise ValidationError(f'{field} is an invalid field type') 18 | try: 19 | print_type( 20 | field.ftype, except_types=(ObjectType, UnionType) 21 | ) 22 | except ValueError: 23 | raise ValidationError( 24 | f'Field type needs be object or built-in type,' 25 | f' rather than {field.ftype}' 26 | ) 27 | if isinstance(field.ftype, InputType): 28 | field.ftype.validate() 29 | 30 | def __str__(cls): 31 | return ( 32 | f'{cls.print_description()}' 33 | + f'input {cls.__name__} ' 34 | + '{\n' 35 | + f'{patch_indents(cls.print_field(), indent=1)}' 36 | + '\n}' 37 | ) 38 | 39 | def print_field(cls, indent=0): 40 | literal = '' 41 | for _, field in cls.__fields__.items(): 42 | literal += f'{field}\n' 43 | return patch_indents(literal[:-1], indent) 44 | 45 | 46 | class Input(metaclass=InputType): 47 | pass 48 | -------------------------------------------------------------------------------- /pygraphy/types/interface.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from pygraphy.utils import ( 3 | to_snake_case, 4 | patch_indents 5 | ) 6 | from .field import ResolverField, FieldableType 7 | 8 | 9 | class InterfaceType(FieldableType): 10 | 11 | def __new__(cls, name, bases, attrs): 12 | cls = super().__new__(cls, name, bases, attrs) 13 | for name in dir(cls): 14 | attr = getattr(cls, name) 15 | field_name = None 16 | if hasattr(attr, '__is_field__'): 17 | field_name = to_snake_case(name) 18 | elif hasattr(attr, '__is_metafield__'): 19 | field_name = f'_{name}' 20 | 21 | if field_name: 22 | sign = inspect.signature(attr) 23 | cls.__fields__[field_name] = ResolverField( 24 | name=field_name, 25 | _ftype=sign.return_annotation, 26 | _params=cls.remove_self(sign.parameters), 27 | description=inspect.getdoc(attr), 28 | _obj=cls 29 | ) 30 | return cls 31 | 32 | @staticmethod 33 | def remove_self(param_dict): 34 | result = {} 35 | first_param = True 36 | for name, param in param_dict.items(): 37 | if first_param: 38 | first_param = False 39 | continue 40 | result[name] = param 41 | return result 42 | 43 | def __str__(cls): 44 | return ( 45 | f'{cls.print_description()}' 46 | + f'interface {cls.__name__} ' 47 | + '{\n' 48 | + f'{patch_indents(cls.print_field(), indent=1)}' 49 | + '\n}' 50 | ) 51 | 52 | 53 | class Interface(metaclass=InterfaceType): 54 | pass 55 | -------------------------------------------------------------------------------- /pygraphy/types/object.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from inspect import isawaitable, _empty 4 | from copy import copy 5 | from graphql.language.ast import ( 6 | FragmentSpreadNode, 7 | InlineFragmentNode 8 | ) 9 | from pygraphy.utils import ( 10 | patch_indents, 11 | to_snake_case, 12 | is_union, 13 | is_list, 14 | is_optional, 15 | shelling_type 16 | ) 17 | from pygraphy import types 18 | from pygraphy.exceptions import RuntimeError, ValidationError 19 | from .interface import InterfaceType 20 | from .field import Field, ResolverField, field, metafield, hidden 21 | from .base import print_type, load_literal_value 22 | 23 | 24 | class ObjectType(InterfaceType): 25 | 26 | def __str__(cls): 27 | return ( 28 | f'{cls.print_description()}' 29 | + f'type {cls.__name__}{cls.print_interface_implement()} ' 30 | + '{\n' 31 | + f'{patch_indents(cls.print_field(), indent=1)}' 32 | + '\n}' 33 | ) 34 | 35 | def print_interface_implement(cls): 36 | literal = '' 37 | for base in cls.__bases__: 38 | if isinstance(base, ObjectType): 39 | continue 40 | if not literal: 41 | literal = f' implements {base.__name__}' 42 | else: 43 | literal += f' & {base.__name__}' 44 | return literal 45 | 46 | def validate(cls): 47 | if cls.__validated__: 48 | return 49 | cls.__validated__ = True 50 | for _, field in cls.__fields__.items(): 51 | if not isinstance(field, (Field, ResolverField)): 52 | raise ValidationError(f'{field} is an invalid field type') 53 | 54 | if field.ftype == _empty: 55 | raise ValidationError(f'The return type of resolver "{cls.__name__}.{field.name}" must not be empty') 56 | 57 | print_type(field.ftype, except_types=(types.InputType)) 58 | 59 | if isinstance(field, ResolverField): 60 | for gtype in field.params.values(): 61 | print_type(gtype) 62 | shelled = shelling_type(gtype) 63 | if isinstance(shelled, types.InputType): 64 | shelled.validate() 65 | if isinstance(field.ftype, ObjectType): 66 | field.ftype.validate() 67 | if is_union(field.ftype): 68 | shelled = shelling_type(field.ftype) 69 | if isinstance(shelled, ObjectType): 70 | shelled.validate() 71 | 72 | 73 | class Object(metaclass=ObjectType): 74 | 75 | def __iter__(self): 76 | for name, value in self.resolve_results.items(): 77 | if isinstance(value, Object): 78 | value = dict(value) 79 | elif isinstance(value, list): 80 | serialized_value = [] 81 | for i in value: 82 | if isinstance(i, Object): 83 | serialized_value.append(dict(i)) 84 | else: 85 | serialized_value.append(i) 86 | value = serialized_value 87 | yield (name, value) 88 | 89 | async def _resolve(self, nodes, error_collector, path=[]): 90 | self.resolve_results = {} 91 | tasks = {} 92 | for node in nodes: 93 | if hasattr(node, 'name'): 94 | path = copy(path) 95 | path.append(node.name.value) 96 | 97 | returned = await self.__resolve_fragment( 98 | node, error_collector, path 99 | ) 100 | if returned: 101 | continue 102 | 103 | name = node.name.value 104 | snake_cases = to_snake_case(name) 105 | field = self.__fields__.get(snake_cases) 106 | keys = self.__dataclass_fields__.keys() 107 | 108 | resolver = self.__get_resover(name, node, field, path) 109 | if not resolver: 110 | try: 111 | tasks[name] = (getattr(self, name if name in keys else snake_cases), node, field, path) 112 | except AttributeError: 113 | raise RuntimeError( 114 | f'{name} is not a valid node of {self}', node, path 115 | ) 116 | else: 117 | kwargs = self.__package_args(node, field, path) 118 | 119 | try: 120 | returned = resolver(**kwargs) 121 | except Exception as e: 122 | self.__handle_error(e, node, path, error_collector) 123 | tasks[name] = (None, node, field, path) 124 | continue 125 | 126 | if isawaitable(returned): 127 | tasks[name] = (asyncio.ensure_future(returned), node, field, path) 128 | else: 129 | tasks[name] = (returned, node, field, path) 130 | 131 | return self.__task_receiver(tasks, error_collector) 132 | 133 | @staticmethod 134 | def __get_field_name(name, node): 135 | if node.alias: 136 | return node.alias.value 137 | return name 138 | 139 | async def __task_receiver(self, tasks, error_collector): 140 | generators = [] 141 | for name, task in tasks.items(): 142 | task, node, field, path = task 143 | if hasattr(task, '__aiter__'): 144 | generators.append(task) 145 | else: 146 | if isawaitable(task): 147 | try: 148 | result = await task 149 | except Exception as e: 150 | self.__handle_error(e, node, path, error_collector) 151 | result = None 152 | else: 153 | result = task 154 | self.resolve_results[self.__get_field_name(name, node)] = result 155 | 156 | for generator in generators: 157 | async for result in generator: 158 | self.resolve_results[self.__get_field_name(name, node)] = result 159 | yield await self.__check_and_circular_resolve(tasks, error_collector) 160 | 161 | if not generators: 162 | yield await self.__check_and_circular_resolve(tasks, error_collector) 163 | 164 | async def __check_and_circular_resolve(self, tasks, error_collector): 165 | for name, task in tasks.items(): 166 | task, node, field, path = task 167 | result = self.resolve_results[self.__get_field_name(name, node)] 168 | if not self.__check_return_type(field.ftype, result): 169 | if result is None and error_collector: 170 | return False 171 | raise RuntimeError( 172 | f'{result} is not a valid return value to' 173 | f' {name}, please check {name}\'s type annotation', 174 | node, 175 | path 176 | ) 177 | await self.__circular_resolve( 178 | result, node, error_collector, path 179 | ) 180 | return self 181 | 182 | @staticmethod 183 | def __handle_error(e, node, path, error_collector): 184 | logging.error(e, exc_info=True) 185 | e.location = node.loc.source.get_location(node.loc.start) 186 | e.path = path 187 | error_collector.append(e) 188 | 189 | async def __circular_resolve(self, result, node, error_collector, path): 190 | if isinstance(result, Object): 191 | async for obj in await result._resolve( 192 | node.selection_set.selections, error_collector, path 193 | ): 194 | pass 195 | elif hasattr(result, '__iter__'): 196 | for item in result: 197 | if isinstance(item, Object): 198 | async for _ in await item._resolve( 199 | node.selection_set.selections, 200 | error_collector, 201 | path 202 | ): 203 | pass 204 | 205 | async def __resolve_fragment(self, node, error_collector, path): 206 | if isinstance(node, InlineFragmentNode): 207 | if node.type_condition.name.value == self.__class__.__name__: 208 | async for _ in await self._resolve( 209 | node.selection_set.selections, 210 | error_collector 211 | ): 212 | pass 213 | return True 214 | elif isinstance(node, FragmentSpreadNode): 215 | root_node = types.context.get().root_ast 216 | for subroot_node in root_node: 217 | if node.name.value == subroot_node.name.value: 218 | current_path = copy(path) 219 | current_path.append(subroot_node.name.value) 220 | async for _ in await self._resolve( 221 | subroot_node.selection_set.selections, 222 | error_collector, 223 | current_path 224 | ): 225 | pass 226 | return True 227 | return False 228 | 229 | def __get_resover(self, name, node, field, path): 230 | snake_cases = to_snake_case(name) 231 | if not field: 232 | raise RuntimeError( 233 | f"Cannot query field '{name}' on type '{type(self)}'.", 234 | node, 235 | path 236 | ) 237 | if not isinstance(field, ResolverField): 238 | return None 239 | if snake_cases.startswith('__'): 240 | resolver = getattr( 241 | self, f'_{snake_cases[2:]}', None 242 | ) 243 | 244 | if not getattr(resolver, '__is_metafield__', False): 245 | resolver = None 246 | else: 247 | resolver = getattr(self, snake_cases, None) 248 | if not getattr(resolver, '__is_field__', False): 249 | resolver = None 250 | if not resolver: 251 | raise RuntimeError( 252 | f"Cannot query field '{name}' on type '{type(self)}'.", 253 | node, 254 | path 255 | ) 256 | return resolver 257 | 258 | def __package_args(self, node, field, path): 259 | kwargs = {} 260 | for arg in node.arguments: 261 | slot = field.params.get(to_snake_case(arg.name.value)) 262 | if not slot: 263 | raise RuntimeError( 264 | f'Can not find {arg.name.value}' 265 | f' as param in {field.name}', 266 | node, 267 | path 268 | ) 269 | kwargs[to_snake_case(arg.name.value)] = load_literal_value( 270 | arg.value, slot 271 | ) 272 | return kwargs 273 | 274 | @classmethod 275 | def __check_return_type(cls, return_type, result): 276 | if is_optional(return_type): 277 | if result is None: 278 | return True 279 | return cls.__check_return_type(return_type.__args__[0], result) 280 | elif is_list(return_type): 281 | if not isinstance(result, list): 282 | return False 283 | if len(result) == 0: 284 | return True 285 | for item in result: 286 | if not cls.__check_return_type(return_type.__args__[0], item): 287 | return False 288 | return True 289 | elif isinstance(return_type, types.UnionType): 290 | for member in return_type.members: 291 | if cls.__check_return_type(member, result): 292 | return True 293 | elif isinstance(result, return_type): 294 | return True 295 | 296 | return False 297 | 298 | 299 | class DefaultObject(Object): 300 | @hidden 301 | @metafield 302 | def _typename(self) -> str: 303 | return type(self).__name__ 304 | -------------------------------------------------------------------------------- /pygraphy/types/schema.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import contextvars 5 | from typing import TypeVar 6 | from abc import abstractmethod, ABC 7 | from graphql.language import parse 8 | from graphql.language.ast import ( 9 | OperationDefinitionNode, 10 | OperationType 11 | ) 12 | from pygraphy.utils import ( 13 | is_union, 14 | is_list, 15 | is_optional, 16 | patch_indents 17 | ) 18 | from pygraphy.encoder import GraphQLEncoder 19 | from pygraphy.exceptions import ValidationError 20 | from pygraphy.context import Context 21 | from .object import ObjectType, Object 22 | from .field import Field, ResolverField 23 | from .union import UnionType 24 | from .input import InputType 25 | from .interface import InterfaceType 26 | from .enum import EnumType 27 | 28 | 29 | class SchemaType(ObjectType): 30 | VALID_ROOT_TYPES = {'query', 'mutation', 'subscription'} 31 | 32 | def __new__(cls, name, bases, attrs): 33 | attrs['registered_type'] = [] 34 | without_dataclass = type.__new__(cls, name, bases, attrs) 35 | 36 | cls = super().__new__(cls, name, bases, attrs) 37 | cls.validated_type = [] 38 | cls.validate() 39 | cls.register_fields_type(cls.__fields__.values()) 40 | 41 | for parent in cls.__mro__: 42 | if hasattr(parent, "__fields__"): 43 | for key, field in parent.__fields__.items(): 44 | if key not in cls.__fields__: 45 | cls.__fields__[key] = field 46 | if hasattr(parent, "registered_type"): 47 | existing_type_name = [t.__name__ for t in cls.registered_type] 48 | for ptype in parent.registered_type: 49 | if ptype.__name__ not in existing_type_name: 50 | cls.registered_type.append(ptype) 51 | 52 | # Schema does not need to be a dataclass 53 | without_dataclass.__fields__ = cls.__fields__ 54 | without_dataclass.__description__ = cls.__description__ 55 | without_dataclass.registered_type = cls.registered_type 56 | return without_dataclass 57 | 58 | def register_fields_type(cls, fields): 59 | param_return_types = [] 60 | for field in fields: 61 | param_return_types.append(field.ftype) 62 | if isinstance(field, ResolverField): 63 | param_return_types.extend(field.params.values()) 64 | cls.register_types(param_return_types) 65 | 66 | def register_types(cls, types): 67 | for ptype in types: 68 | if ptype in cls.validated_type: 69 | continue 70 | cls.validated_type.append(ptype) 71 | 72 | if isinstance(ptype, ObjectType): 73 | cls.registered_type.append(ptype) 74 | cls.register_fields_type(ptype.__fields__.values()) 75 | elif is_union(ptype) or is_list(ptype): 76 | cls.register_types(ptype.__args__) 77 | elif isinstance(ptype, UnionType): 78 | cls.registered_type.append(ptype) 79 | cls.register_types(ptype.members) 80 | elif isinstance(ptype, InputType): 81 | cls.registered_type.append(ptype) 82 | cls.register_fields_type(ptype.__fields__.values()) 83 | elif isinstance(ptype, InterfaceType): 84 | cls.registered_type.append(ptype) 85 | cls.register_fields_type(ptype.__fields__.values()) 86 | cls.register_types(ptype.__subclasses__()) 87 | elif isinstance(ptype, EnumType): 88 | cls.registered_type.append(ptype) 89 | else: 90 | # Other basic types, do not need be handled 91 | pass 92 | 93 | def validate(cls): 94 | for name, field in cls.__fields__.items(): 95 | if name not in cls.VALID_ROOT_TYPES: 96 | raise ValidationError( 97 | f'The valid root type must be {cls.VALID_ROOT_TYPES},' 98 | f' rather than {name}' 99 | ) 100 | if not isinstance(field, Field): 101 | raise ValidationError(f'{field} is an invalid field type') 102 | if not is_optional(field.ftype): 103 | raise ValidationError( 104 | f'The return type of root object should be Optional' 105 | ) 106 | if not isinstance(field.ftype.__args__[0], ObjectType): 107 | raise ValidationError( 108 | f'The typt of root object must be an Object, rather than {field.ftype}' 109 | ) 110 | ObjectType.validate(cls) 111 | 112 | def __str__(cls): 113 | string = '' 114 | for rtype in cls.registered_type: 115 | string += (str(rtype) + '\n\n') 116 | schema = ( 117 | f'{cls.print_description()}' 118 | + f'schema ' 119 | + '{\n' 120 | + f'{patch_indents(cls.print_field(), indent=1)}' 121 | + '\n}' 122 | ) 123 | return string + schema 124 | 125 | 126 | context: contextvars.ContextVar[Context] = contextvars.ContextVar('context') 127 | 128 | 129 | class Schema(Object, metaclass=SchemaType): 130 | 131 | OPERATION_MAP = { 132 | OperationType.QUERY: 'query', 133 | OperationType.MUTATION: 'mutation', 134 | } 135 | 136 | @classmethod 137 | async def execute(cls, query, variables=None, request=None, serialize=False): 138 | document = parse(query) 139 | operation_result = { 140 | 'errors': None, 141 | 'data': None 142 | } 143 | for definition in document.definitions: 144 | if not isinstance(definition, OperationDefinitionNode): 145 | continue 146 | 147 | if definition.operation not in cls.OPERATION_MAP \ 148 | or cls.OPERATION_MAP[definition.operation] not in cls.__fields__: 149 | operation_result = { 150 | 'errors': { 151 | 'message': 'This API does not support this operation' 152 | }, 153 | 'data': None 154 | } 155 | break 156 | async for operation_result in cls._execute_operation( 157 | document, 158 | definition, 159 | variables, 160 | request 161 | ): 162 | pass 163 | 164 | if serialize: 165 | return json.dumps(operation_result, cls=GraphQLEncoder) 166 | else: 167 | return operation_result 168 | 169 | @classmethod 170 | async def _execute_operation(cls, document, definition, variables, request): 171 | obj = cls.__fields__[ 172 | cls.OPERATION_MAP[definition.operation] 173 | ].ftype.__args__[0]() 174 | error_collector = [] 175 | token = context.set( 176 | Context( 177 | schema=cls, 178 | root_ast=document.definitions, 179 | request=request, 180 | variables=variables 181 | ) 182 | ) 183 | try: 184 | async for obj in await obj._resolve( 185 | definition.selection_set.selections, 186 | error_collector 187 | ): 188 | return_root = { 189 | 'errors': error_collector if error_collector else None, 190 | 'data': dict(obj) if obj else None 191 | } 192 | yield return_root 193 | except Exception as e: 194 | logging.error(e, exc_info=True) 195 | error_collector.append(e) 196 | finally: 197 | context.reset(token) 198 | 199 | 200 | class Socket(ABC): 201 | 202 | @abstractmethod 203 | async def send(self, text: str): 204 | pass 205 | 206 | @abstractmethod 207 | async def receive(self) -> str: 208 | pass 209 | 210 | @abstractmethod 211 | async def close(self): 212 | pass 213 | 214 | 215 | T = TypeVar("T", bound=Socket) 216 | 217 | 218 | class SubscribableSchema(Schema): 219 | OPERATION_MAP = { 220 | OperationType.QUERY: 'query', 221 | OperationType.MUTATION: 'mutation', 222 | OperationType.SUBSCRIPTION: 'subscription' 223 | } 224 | 225 | @classmethod 226 | async def execute(cls, socket: T): 227 | subscription_router = {} 228 | 229 | while True: 230 | try: 231 | message = await socket.receive() 232 | except Exception: 233 | await socket.close() 234 | return 235 | try: 236 | data = json.loads(message) 237 | except Exception as e: 238 | logging.error(e, exc_info=True) 239 | await cls.send_connection_error(socket, e) 240 | continue 241 | 242 | query_type, payload = data['type'], data.get('payload') 243 | if query_type == 'connection_init': 244 | asyncio.ensure_future(cls.start_ack_loop(socket)) 245 | elif query_type == 'start': 246 | id = data['id'] 247 | variables, query = payload['variables'], payload['query'] 248 | task = asyncio.ensure_future(cls.subscribe(socket, id, query, variables)) 249 | subscription_router[id] = task 250 | elif query_type == 'stop': 251 | id = data['id'] 252 | task = subscription_router.get(id) 253 | if task: 254 | task.cancel() 255 | del subscription_router[id] 256 | else: 257 | await cls.send_connection_error(socket, f'Unsupported message type {repr(query_type)}') 258 | return 259 | 260 | @classmethod 261 | async def subscribe(cls, socket, id, query, variables): 262 | document = parse(query) 263 | for definition in document.definitions: 264 | if not isinstance(definition, OperationDefinitionNode): 265 | continue 266 | 267 | if cls.OPERATION_MAP[definition.operation] not in cls.__fields__: 268 | await cls.send_error(socket, id, 'This API does not support this operation') 269 | break 270 | 271 | async for operation_result in cls._execute_operation( 272 | document, definition, variables, socket 273 | ): 274 | try: 275 | await socket.send( 276 | json.dumps({ 277 | 'type': 'data', 278 | 'id': id, 279 | 'payload': operation_result 280 | }, 281 | cls=GraphQLEncoder 282 | ) 283 | ) 284 | except Exception as e: 285 | logging.error(e, exc_info=True) 286 | raise 287 | try: 288 | await socket.send( 289 | json.dumps({ 290 | 'type': 'complete', 291 | 'id': id, 292 | }, 293 | cls=GraphQLEncoder 294 | ) 295 | ) 296 | except Exception as e: 297 | logging.error(e, exc_info=True) 298 | raise 299 | break 300 | 301 | @staticmethod 302 | async def send_error(socket, id, e): 303 | try: 304 | await socket.send( 305 | json.dumps({ 306 | 'type': 'error', 307 | 'id': id, 308 | 'payload': { 309 | 'errors': { 310 | 'message': e 311 | }, 312 | 'data': None 313 | } 314 | }, 315 | cls=GraphQLEncoder 316 | ) 317 | ) 318 | except Exception as e: 319 | logging.error(e, exc_info=True) 320 | raise 321 | 322 | @staticmethod 323 | async def send_connection_error(socket, e): 324 | try: 325 | await socket.send( 326 | json.dumps({ 327 | 'type': 'connection_error', 328 | 'payload': { 329 | 'errors': { 330 | 'message': e 331 | }, 332 | 'data': None 333 | } 334 | }, 335 | cls=GraphQLEncoder 336 | ) 337 | ) 338 | except Exception as e: 339 | logging.error(e, exc_info=True) 340 | raise 341 | 342 | @staticmethod 343 | async def start_ack_loop(socket, sleep=20): 344 | try: 345 | await socket.send( 346 | json.dumps({ 347 | 'type': 'connection_ack' 348 | }) 349 | ) 350 | except RuntimeError: 351 | # socket closed 352 | return 353 | while True: 354 | try: 355 | await socket.send( 356 | json.dumps({ 357 | 'type': 'ka' 358 | }) 359 | ) 360 | except RuntimeError: 361 | # socket closed 362 | return 363 | await asyncio.sleep(sleep) 364 | -------------------------------------------------------------------------------- /pygraphy/types/union.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from pygraphy.utils import patch_indents 3 | from pygraphy.exceptions import ValidationError 4 | from .base import GraphQLType 5 | from .object import ObjectType 6 | 7 | 8 | class UnionType(GraphQLType): 9 | 10 | def __new__(cls, name, bases, attrs): 11 | if 'members' not in attrs: 12 | raise ValidationError('Union type must has members attribute') 13 | if not isinstance(attrs['members'], tuple): 14 | raise ValidationError('Members must be tuple') 15 | for member in attrs['members']: 16 | if not isinstance(member, ObjectType): 17 | raise ValidationError('The member of Union type must be Object') 18 | return super().__new__(cls, name, bases, attrs) 19 | 20 | def __str__(cls): 21 | description = inspect.getdoc(cls) 22 | description_literal = f'"""\n{description}\n"""\n' if description else '' # noqa 23 | return ( 24 | description_literal 25 | + f'union {cls.__name__} =\n' 26 | + f'{patch_indents(cls.print_union_member(), indent=1)}' 27 | ) 28 | 29 | def print_union_member(cls): 30 | literal = '' 31 | for ptype in cls.members: 32 | literal += f'| {ptype.__name__}\n' 33 | return literal[:-1] if literal.endswith('\n') else literal 34 | 35 | 36 | class Union(metaclass=UnionType): 37 | members = () 38 | -------------------------------------------------------------------------------- /pygraphy/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import typing 3 | 4 | 5 | def patch_indents(string, indent=0): 6 | spaces = ' ' * indent 7 | return spaces + string.replace('\n', '\n' + spaces) 8 | 9 | 10 | def is_union(annotation): 11 | """Returns True if annotation is a typing.Union""" 12 | 13 | annotation_origin = getattr(annotation, "__origin__", None) 14 | 15 | return annotation_origin == typing.Union 16 | 17 | 18 | def is_optional(annotation): 19 | annotation_origin = getattr(annotation, "__origin__", None) 20 | return annotation_origin == typing.Union \ 21 | and len(annotation.__args__) == 2 \ 22 | and annotation.__args__[1] == type(None) # noqa 23 | 24 | 25 | def is_list(annotation): 26 | return getattr(annotation, "__origin__", None) == list 27 | 28 | 29 | def to_camel_case(name): 30 | has_prefix = False 31 | if '__' in name: 32 | has_prefix = True 33 | without_prifix_name = ''.join(name.split('__')[1:]) 34 | else: 35 | without_prifix_name = name 36 | components = without_prifix_name.split("_") 37 | res = components[0] + "".join(x.capitalize() if x else "_" for x in components[1:]) # noqa 38 | return '__' + res if has_prefix else res 39 | 40 | 41 | seprate_upper_case = re.compile("(.)([A-Z][a-z]+)") 42 | seprate_upper_case_behind_lower_case = re.compile("([a-z0-9])([A-Z])") 43 | 44 | 45 | def to_snake_case(name): 46 | has_prefix = False 47 | if '__' in name: 48 | has_prefix = True 49 | without_prifix_name = ''.join(name.split('__')[1:]) 50 | else: 51 | without_prifix_name = name 52 | s1 = seprate_upper_case.sub(r"\1_\2", without_prifix_name) 53 | res = seprate_upper_case_behind_lower_case.sub(r"\1_\2", s1).lower() 54 | return '__' + res if has_prefix else res 55 | 56 | 57 | def meta(obj): 58 | obj.__name__ = '__' + obj.__name__ 59 | return obj 60 | 61 | 62 | def shelling_type(type): 63 | while is_optional(type) or is_list(type): 64 | type = type.__args__[0] 65 | return type 66 | -------------------------------------------------------------------------------- /pygraphy/view.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | import dataclasses 4 | from starlette import status 5 | from starlette.websockets import WebSocket 6 | from starlette.endpoints import HTTPEndpoint, WebSocketEndpoint 7 | from starlette.responses import PlainTextResponse, HTMLResponse, Response 8 | from .introspection import WithMetaSchema, WithMetaSubSchema 9 | from .encoder import GraphQLEncoder 10 | from .types.schema import Socket 11 | 12 | 13 | def get_playground_html(request_path: str, settings: str) -> str: 14 | here = pathlib.Path(__file__).parents[0] 15 | path = here / "static/playground.html" 16 | 17 | with open(path) as f: 18 | template = f.read() 19 | 20 | return template.replace("{{REQUEST_PATH}}", request_path)\ 21 | .replace("{{SETTINGS}}", json.dumps(settings)) 22 | 23 | 24 | class Schema(HTTPEndpoint, WithMetaSchema): 25 | 26 | PLAYGROUND_SETTINGS = {} 27 | 28 | async def get(self, request): 29 | html = get_playground_html(request.url.path, self.PLAYGROUND_SETTINGS) 30 | return HTMLResponse(html) 31 | 32 | async def post(self, request): 33 | content_type = request.headers.get("Content-Type", "") 34 | 35 | if "application/json" in content_type: 36 | data = await request.json() 37 | elif "application/graphql" in content_type: 38 | body = await request.body() 39 | text = body.decode() 40 | data = {"query": text} 41 | elif "query" in request.query_params: 42 | data = request.query_params 43 | else: 44 | return PlainTextResponse( 45 | "Unsupported Media Type", 46 | status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, 47 | ) 48 | 49 | try: 50 | query = data["query"] 51 | variables = data.get("variables") 52 | except KeyError: 53 | return PlainTextResponse( 54 | "No GraphQL query found in the request", 55 | status_code=status.HTTP_400_BAD_REQUEST, 56 | ) 57 | 58 | result = await self.execute( 59 | query, variables=variables, request=request 60 | ) 61 | status_code = status.HTTP_200_OK 62 | return Response( 63 | json.dumps(result, cls=GraphQLEncoder), 64 | status_code=status_code, 65 | media_type='application/json' 66 | ) 67 | 68 | 69 | @dataclasses.dataclass 70 | class StarletteSocket(Socket): 71 | websocket: WebSocket 72 | 73 | async def send(self, text): 74 | return await self.websocket.send_text(text) 75 | 76 | async def receive(self): 77 | return await self.websocket.receive_text() 78 | 79 | async def close(self): 80 | # We have handled close event in schema executor, so reset it 81 | from starlette.websockets import WebSocketState 82 | self.websocket.client_state = WebSocketState.CONNECTED 83 | 84 | async def fake_receiver(): 85 | return {"type": "websocket.disconnect"} 86 | self.websocket._receive = fake_receiver 87 | 88 | 89 | class SubscribableSchema(WebSocketEndpoint, WithMetaSubSchema): 90 | 91 | async def on_connect(self, websocket): 92 | await websocket.accept(subprotocol='graphql-ws') 93 | socket = StarletteSocket(websocket) 94 | try: 95 | await self.execute( 96 | socket 97 | ) 98 | finally: 99 | await websocket.close() 100 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE 3 | 4 | [bdist_wheel] 5 | python-tag = py3 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | 6 | from os.path import join, dirname 7 | 8 | from setuptools import setup 9 | 10 | with open(join(dirname(__file__), 'pygraphy', '__init__.py'), 'r') as f: 11 | version = re.match(r".*__version__ = '(.*?)'", f.read(), re.S).group(1) 12 | 13 | install_requires = [ 14 | "GraphQL-core-next>=1.1.0,<1.2.0" 15 | ] 16 | 17 | dev_requires = [ 18 | "flake8>=3.7.7", 19 | "pytest>=5.0.0", 20 | ] 21 | 22 | 23 | setup( 24 | name="pygraphy", 25 | version=version, 26 | description="A modern pythonic implementation of GraphQL.", 27 | long_description=open("README.md").read(), 28 | long_description_content_type="text/markdown", 29 | keywords="python graphql", 30 | author="Tzu-sing Gwo", 31 | author_email="zi-xing.guo@ubisoft.com", 32 | url="https://github.com/ethe/pygraphy", 33 | license="MIT", 34 | packages=['pygraphy'], 35 | include_package_data=True, 36 | install_requires=install_requires, 37 | tests_require=dev_requires, 38 | python_requires=">=3.7,<4", 39 | extras_require={ 40 | "dev": dev_requires, 41 | "web": ["starlette>=0.12.1,<0.13.0"] 42 | }, 43 | classifiers=[ 44 | "Topic :: Software Development", 45 | "Development Status :: 1 - Planning", 46 | "Intended Audience :: Developers", 47 | "License :: OSI Approved :: MIT License", 48 | "Programming Language :: Python :: 3", 49 | "Programming Language :: Python :: 3.7", 50 | "Programming Language :: Python :: 3.8", 51 | "Programming Language :: Python :: Implementation :: CPython", 52 | "Programming Language :: Python :: Implementation :: PyPy", 53 | ] 54 | ) 55 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethe/pygraphy/fe0f632af0d44d1472a2d06c31758494f741c661/tests/__init__.py -------------------------------------------------------------------------------- /tests/introspection_result: -------------------------------------------------------------------------------- 1 | {"errors": null, "data": {"__schema": {"queryType": {"name": "Query"}, "mutationType": null, "subscriptionType": null, "types": [{"kind": "SCALAR", "name": "Int", "description": "int([x]) -> integer\nint(x, base=10) -> integer\n\nConvert a number or string to an integer, or return 0 if no arguments\nare given. If x is a number, return x.__int__(). For floating point\nnumbers, this truncates towards zero.\n\nIf x is not a number or if base is given, then x must be a string,\nbytes, or bytearray instance representing an integer literal in the\ngiven base. The literal can be preceded by '+' or '-' and be surrounded\nby whitespace. The base defaults to 10. Valid bases are 0 and 2-36.\nBase 0 means to interpret the base from the string as an integer literal.\n>>> int('0b100', base=0)\n4", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "Float", "description": "Convert a string or number to a floating point number, if possible.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "String", "description": "str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "Boolean", "description": "bool(x) -> bool\n\nReturns True when the argument x is true, False otherwise.\nThe builtins True and False are the only two instances of the class bool.\nThe class bool is a subclass of the class int, and cannot be subclassed.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "Query", "description": "Query()", "fields": [{"name": "__schema", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Schema", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__type", "description": null, "args": [{"name": "name", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "droid", "description": null, "args": [{"name": "id", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "Droid", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "hero", "description": null, "args": [{"name": "episode", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "Episode", "ofType": null}}, "defaultValue": null}], "type": {"kind": "INTERFACE", "name": "Character", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "human", "description": null, "args": [{"name": "id", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "defaultValue": "\"1234\""}], "type": {"kind": "OBJECT", "name": "Human", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Schema", "description": "Schema()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "directives", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Directive", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "mutationType", "description": "If this server supports mutation, the type that mutation operations will be rooted at.", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "queryType", "description": "The type that query operations will be rooted at.", "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "subscriptionType", "description": "Not support yet", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "types", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Directive", "description": "Directive(name: str, description: Union[str, NoneType], locations: List[pygraphy.introspection.DirectiveLocation], args: List[ForwardRef('InputValue')])", "fields": [{"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "locations", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "__DirectiveLocation", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "args", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "__DirectiveLocation", "description": "An enumeration.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "ARGUMENT_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "ENUM", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "ENUM_VALUE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FIELD", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FIELD_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FRAGMENT_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FRAGMENT_SPREAD", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INLINE_FRAGMENT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_FIELD_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INTERFACE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "MUTATION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "QUERY", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCALAR", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCHEMA", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SUBSCRIPTION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "UNION", "description": null, "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "OBJECT", "name": "__InputValue", "description": "InputValue()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "defaultValue", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": "Not support yet", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "type", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Type", "description": "Type()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "enumValues", "description": "ENUM only", "args": [{"name": "includeDeprecated", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": "false"}], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__EnumValue", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "fields", "description": "OBJECT and INTERFACE only", "args": [{"name": "includeDeprecated", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": "false"}], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Field", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "inputFields", "description": "INPUT_OBJECT only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "interfaces", "description": "OBJECT only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "kind", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "__TypeKind", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "ofType", "description": "NON_NULL and LIST only", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "possibleTypes", "description": "INTERFACE and UNION only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__EnumValue", "description": "EnumValue(name: str, description: Union[str, NoneType], is_deprecated: bool, deprecation_reason: Union[str, NoneType])", "fields": [{"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "isDeprecated", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "deprecationReason", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Field", "description": "Field()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "args", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "deprecationReason", "description": "Not support yet", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "isDeprecated", "description": "Not support yet", "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "type", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "__TypeKind", "description": "An enumeration.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "ENUM", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INTERFACE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "LIST", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "NON_NULL", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCALAR", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "UNION", "description": null, "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "OBJECT", "name": "Droid", "description": "Droid object", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "appearsIn", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "Episode", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "primaryFunction", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "friends", "description": null, "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INTERFACE", "name": "Character", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [{"kind": "INTERFACE", "name": "Character", "ofType": null}], "enumValues": null, "possibleTypes": []}, {"kind": "ENUM", "name": "Episode", "description": "An enumeration.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "EMPIRE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "JEDI", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "NEWHOPE", "description": null, "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "INTERFACE", "name": "Character", "description": "Character interface contains human and droid", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "appearsIn", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "Episode", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "friends", "description": null, "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INTERFACE", "name": "Character", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": [{"kind": "OBJECT", "name": "Human", "ofType": null}, {"kind": "OBJECT", "name": "Droid", "ofType": null}]}, {"kind": "OBJECT", "name": "Human", "description": "Human object", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "appearsIn", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "Episode", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "homePlanet", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "friends", "description": null, "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INTERFACE", "name": "Character", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [{"kind": "INTERFACE", "name": "Character", "ofType": null}], "enumValues": null, "possibleTypes": []}], "directives": []}}} 2 | {"errors": null, "data": {"__schema": {"queryType": {"name": "Query"}, "mutationType": {"name": "Mutation"}, "subscriptionType": null, "types": [{"kind": "SCALAR", "name": "Int", "description": "int([x]) -> integer\nint(x, base=10) -> integer\n\nConvert a number or string to an integer, or return 0 if no arguments\nare given. If x is a number, return x.__int__(). For floating point\nnumbers, this truncates towards zero.\n\nIf x is not a number or if base is given, then x must be a string,\nbytes, or bytearray instance representing an integer literal in the\ngiven base. The literal can be preceded by '+' or '-' and be surrounded\nby whitespace. The base defaults to 10. Valid bases are 0 and 2-36.\nBase 0 means to interpret the base from the string as an integer literal.\n>>> int('0b100', base=0)\n4", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "Float", "description": "Convert a string or number to a floating point number, if possible.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "String", "description": "str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "Boolean", "description": "bool(x) -> bool\n\nReturns True when the argument x is true, False otherwise.\nThe builtins True and False are the only two instances of the class bool.\nThe class bool is a subclass of the class int, and cannot be subclassed.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "Query", "description": "Query()", "fields": [{"name": "__schema", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Schema", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__type", "description": null, "args": [{"name": "name", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "address", "description": null, "args": [{"name": "geo", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "GeoInput", "ofType": null}}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "Address", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Schema", "description": "Schema()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "directives", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Directive", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "mutationType", "description": "If this server supports mutation, the type that mutation operations will be rooted at.", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "queryType", "description": "The type that query operations will be rooted at.", "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "subscriptionType", "description": "Not support yet", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "types", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Directive", "description": "Directive(name: str, description: Union[str, NoneType], locations: List[pygraphy.introspection.DirectiveLocation], args: List[ForwardRef('InputValue')])", "fields": [{"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "locations", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "__DirectiveLocation", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "args", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "__DirectiveLocation", "description": "An enumeration.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "ARGUMENT_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "ENUM", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "ENUM_VALUE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FIELD", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FIELD_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FRAGMENT_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FRAGMENT_SPREAD", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INLINE_FRAGMENT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_FIELD_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INTERFACE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "MUTATION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "QUERY", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCALAR", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCHEMA", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SUBSCRIPTION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "UNION", "description": null, "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "OBJECT", "name": "__InputValue", "description": "InputValue()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "defaultValue", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": "Not support yet", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "type", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Type", "description": "Type()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "enumValues", "description": "ENUM only", "args": [{"name": "includeDeprecated", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": "false"}], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__EnumValue", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "fields", "description": "OBJECT and INTERFACE only", "args": [{"name": "includeDeprecated", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": "false"}], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Field", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "inputFields", "description": "INPUT_OBJECT only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "interfaces", "description": "OBJECT only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "kind", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "__TypeKind", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "ofType", "description": "NON_NULL and LIST only", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "possibleTypes", "description": "INTERFACE and UNION only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__EnumValue", "description": "EnumValue(name: str, description: Union[str, NoneType], is_deprecated: bool, deprecation_reason: Union[str, NoneType])", "fields": [{"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "isDeprecated", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "deprecationReason", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Field", "description": "Field()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "args", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "deprecationReason", "description": "Not support yet", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "isDeprecated", "description": "Not support yet", "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "type", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "__TypeKind", "description": "An enumeration.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "ENUM", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INTERFACE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "LIST", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "NON_NULL", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCALAR", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "UNION", "description": null, "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "OBJECT", "name": "Address", "description": "Address(latlng: str)", "fields": [{"name": "latlng", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "foobar", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "UNION", "name": "FooBar", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "UNION", "name": "FooBar", "description": null, "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": [{"kind": "OBJECT", "name": "Foo", "ofType": null}, {"kind": "OBJECT", "name": "Bar", "ofType": null}]}, {"kind": "OBJECT", "name": "Foo", "description": "Foo(a: str)", "fields": [{"name": "a", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "Bar", "description": "Bar(b: int)", "fields": [{"name": "b", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "GeoInput", "description": "GeoInput(lat: float, lng: float)", "fields": null, "inputFields": [{"name": "lat", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Float", "ofType": null}}, "defaultValue": null}, {"name": "lng", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Float", "ofType": null}}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "Mutation", "description": "Mutation()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "createAddress", "description": null, "args": [{"name": "geo", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "GeoInput", "ofType": null}}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "Address", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}], "directives": []}}} 3 | -------------------------------------------------------------------------------- /tests/subscription_introspection: -------------------------------------------------------------------------------- 1 | {"type": "data", "id": 2, "payload": {"errors": null, "data": {"__schema": {"queryType": {"name": "Query"}, "mutationType": null, "subscriptionType": null, "types": [{"kind": "SCALAR", "name": "Int", "description": "int([x]) -> integer\nint(x, base=10) -> integer\n\nConvert a number or string to an integer, or return 0 if no arguments\nare given. If x is a number, return x.__int__(). For floating point\nnumbers, this truncates towards zero.\n\nIf x is not a number or if base is given, then x must be a string,\nbytes, or bytearray instance representing an integer literal in the\ngiven base. The literal can be preceded by '+' or '-' and be surrounded\nby whitespace. The base defaults to 10. Valid bases are 0 and 2-36.\nBase 0 means to interpret the base from the string as an integer literal.\n>>> int('0b100', base=0)\n4", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "Float", "description": "Convert a string or number to a floating point number, if possible.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "String", "description": "str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "Boolean", "description": "bool(x) -> bool\n\nReturns True when the argument x is true, False otherwise.\nThe builtins True and False are the only two instances of the class bool.\nThe class bool is a subclass of the class int, and cannot be subclassed.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "Subscription", "description": "Subscription()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "beat", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "Beat", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "Beat", "description": "Beat(beat: int)", "fields": [{"name": "beat", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "foo", "description": null, "args": [{"name": "arg", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "Query", "description": "Query()", "fields": [{"name": "__schema", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Schema", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__type", "description": null, "args": [{"name": "name", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Schema", "description": "Schema()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "directives", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Directive", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "mutationType", "description": "If this server supports mutation, the type that mutation operations will be rooted at.", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "queryType", "description": "The type that query operations will be rooted at.", "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "subscriptionType", "description": "Not support yet", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "types", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Directive", "description": "Directive(name: str, description: Union[str, NoneType], locations: List[pygraphy.introspection.DirectiveLocation], args: List[ForwardRef('InputValue')])", "fields": [{"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "locations", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "__DirectiveLocation", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "args", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "__DirectiveLocation", "description": "An enumeration.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "ARGUMENT_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "ENUM", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "ENUM_VALUE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FIELD", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FIELD_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FRAGMENT_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "FRAGMENT_SPREAD", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INLINE_FRAGMENT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_FIELD_DEFINITION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INTERFACE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "MUTATION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "QUERY", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCALAR", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCHEMA", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SUBSCRIPTION", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "UNION", "description": null, "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "OBJECT", "name": "__InputValue", "description": "InputValue()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "defaultValue", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": "Not support yet", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "type", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Type", "description": "Type()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "enumValues", "description": "ENUM only", "args": [{"name": "includeDeprecated", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": "false"}], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__EnumValue", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "fields", "description": "OBJECT and INTERFACE only", "args": [{"name": "includeDeprecated", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": "false"}], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Field", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "inputFields", "description": "INPUT_OBJECT only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "interfaces", "description": "OBJECT only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "kind", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "__TypeKind", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "ofType", "description": "NON_NULL and LIST only", "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "possibleTypes", "description": "INTERFACE and UNION only", "args": [], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__EnumValue", "description": "EnumValue(name: str, description: Union[str, NoneType], is_deprecated: bool, deprecation_reason: Union[str, NoneType])", "fields": [{"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "isDeprecated", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "deprecationReason", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Field", "description": "Field()", "fields": [{"name": "__typename", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "args", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "deprecationReason", "description": "Not support yet", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "isDeprecated", "description": "Not support yet", "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "type", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "__Type", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "__TypeKind", "description": "An enumeration.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "ENUM", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INTERFACE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "LIST", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "NON_NULL", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCALAR", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "UNION", "description": null, "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}], "directives": []}}}} 2 | -------------------------------------------------------------------------------- /tests/test_asyncio.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | import pygraphy 4 | from typing import Optional 5 | 6 | 7 | pytestmark = pytest.mark.asyncio 8 | global_var = False 9 | 10 | 11 | class Query(pygraphy.Query): 12 | 13 | @pygraphy.field 14 | async def foo(self) -> bool: 15 | global global_var 16 | result = global_var 17 | await asyncio.sleep(0.1) 18 | global_var = True 19 | return result 20 | 21 | @pygraphy.field 22 | async def bar(self) -> bool: 23 | global global_var 24 | result = global_var 25 | await asyncio.sleep(0.1) 26 | global_var = True 27 | return result 28 | 29 | 30 | class Schema(pygraphy.Schema): 31 | query: Optional[Query] 32 | 33 | 34 | async def test_asyncio(): 35 | query = """ 36 | query test { 37 | foo 38 | bar 39 | } 40 | """ 41 | 42 | # Obviously, foo and bar both return False 43 | assert await Schema.execute(query, serialize=True) == r'{"errors": null, "data": {"foo": false, "bar": false}}' 44 | -------------------------------------------------------------------------------- /tests/test_definition.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union, Dict, List 2 | from inspect import _ParameterKind, Parameter 3 | from pygraphy.types import ( 4 | Object, 5 | Enum, 6 | Union as GraphQLUnion, 7 | Input, 8 | Schema, 9 | Interface, 10 | field 11 | ) 12 | from pygraphy.types.field import Field, ResolverField 13 | from pygraphy.exceptions import ValidationError 14 | 15 | 16 | def test_model_definition(): 17 | class Foo(Object): 18 | "description bar" 19 | a: str 20 | @field 21 | def foo(self, a: int) -> Optional[str]: 22 | "description foo" 23 | pass 24 | 25 | assert Foo.__fields__ == { 26 | '__typename': ResolverField( 27 | _obj=Foo, 28 | name='__typename', 29 | _ftype=str, 30 | description=None, 31 | _params={} 32 | ), 33 | 'a': Field(name='a', _ftype=str, description=None, _obj=Foo), 34 | 'foo': ResolverField( 35 | name='foo', 36 | _ftype=Union[str, None], 37 | description='description foo', 38 | _params={'a': Parameter( 39 | 'a', _ParameterKind.POSITIONAL_OR_KEYWORD, annotation=int 40 | ) 41 | }, 42 | _obj=Foo 43 | ) 44 | } 45 | assert Foo.__description__ == "description bar" 46 | 47 | try: 48 | class Foo(Object): 49 | @field 50 | def foo(self, a: int) -> Dict: 51 | "description foo" 52 | pass 53 | 54 | str(Foo) 55 | except ValidationError: 56 | return 57 | assert False # never reached 58 | 59 | 60 | def test_resolver_field_definition(): 61 | class Foo(Object): 62 | "description bar" 63 | a: str 64 | @field 65 | def foo(self, a: int) -> Optional[str]: 66 | "description foo" 67 | pass 68 | 69 | foo_field = Foo.__fields__['foo'] 70 | assert foo_field.description == "description foo" 71 | assert foo_field.params['a'] == int 72 | 73 | 74 | def test_field_definition(): 75 | class Foo(Object): 76 | "description bar" 77 | a: Optional[str] 78 | 79 | assert Foo.__fields__['a'].ftype == Optional[str] 80 | 81 | 82 | def test_model_literal(): 83 | class Foo(Object): 84 | "description bar" 85 | a: str 86 | @field 87 | def foo(self, a: int) -> Optional[List[str]]: 88 | "description foo" 89 | pass 90 | assert str(Foo) == '"""\ndescription bar\n"""\ntype Foo {\n a: String!\n "description foo"\n foo(\n a: Int!\n ): [String!]\n}' 91 | 92 | 93 | def test_enum_definition(): 94 | class Foo(Enum): 95 | BAR = 1 96 | BAZ = 2 97 | assert str(Foo) == '''""" 98 | An enumeration. 99 | """ 100 | enum Foo { 101 | BAR 102 | BAZ 103 | }''' 104 | 105 | 106 | def test_union_definition(): 107 | class Foo(Object): 108 | a: int 109 | 110 | class Bar(Object): 111 | a: str 112 | 113 | class FooBar(GraphQLUnion): 114 | members = (Foo, Bar) 115 | 116 | assert str(Foo) == '"""\nFoo(a: int)\n"""\ntype Foo {\n a: Int!\n}' 117 | 118 | try: 119 | class FooInt(GraphQLUnion): 120 | members = (Foo, int) 121 | except ValidationError: 122 | return 123 | assert False 124 | 125 | 126 | def test_input_definition(): 127 | class Foo(Input): 128 | a: str 129 | 130 | class Bar(Input): 131 | b: Foo 132 | 133 | class Query(Object): 134 | @field 135 | def foo_a(self, a: Bar) -> Optional[str]: 136 | return 'test' 137 | 138 | class PySchema(Schema): 139 | query: Optional[Query] 140 | 141 | assert str(PySchema) == '''""" 142 | Query() 143 | """ 144 | type Query { 145 | fooA( 146 | a: Bar! 147 | ): String 148 | } 149 | 150 | """ 151 | Bar(b: tests.test_definition.test_input_definition..Foo) 152 | """ 153 | input Bar { 154 | b: Foo! 155 | } 156 | 157 | """ 158 | Foo(a: str) 159 | """ 160 | input Foo { 161 | a: String! 162 | } 163 | 164 | """ 165 | PySchema(query: Union[tests.test_definition.test_input_definition..Query, NoneType]) 166 | """ 167 | schema { 168 | query: Query 169 | }''' 170 | 171 | 172 | def test_schema_definition(): 173 | class Foo(Object): 174 | a: str 175 | 176 | class Bar(Object): 177 | a: int 178 | 179 | class FooBar(GraphQLUnion): 180 | members = (Foo, Bar) 181 | 182 | class Query(Object): 183 | @field 184 | def foo_a(self, a: FooBar) -> Optional[str]: 185 | return 'test' 186 | 187 | class PySchema(Schema): 188 | query: Optional[Query] 189 | 190 | assert str(PySchema) == '''""" 191 | Query() 192 | """ 193 | type Query { 194 | fooA( 195 | a: FooBar! 196 | ): String 197 | } 198 | 199 | union FooBar = 200 | | Foo 201 | | Bar 202 | 203 | """ 204 | Foo(a: str) 205 | """ 206 | type Foo { 207 | a: String! 208 | } 209 | 210 | """ 211 | Bar(a: int) 212 | """ 213 | type Bar { 214 | a: Int! 215 | } 216 | 217 | """ 218 | PySchema(query: Union[tests.test_definition.test_schema_definition..Query, NoneType]) 219 | """ 220 | schema { 221 | query: Query 222 | }''' 223 | 224 | 225 | # the type which literal annotation refers to should be defined in top lovel 226 | class Foo(Object): 227 | a: Optional['Bar'] 228 | 229 | 230 | class Bar(Object): 231 | a: Optional['Foo'] 232 | 233 | 234 | def test_circular_definition(): 235 | class Query(Object): 236 | @field 237 | def foo_a(self, a: str) -> 'Bar': 238 | return 'test' 239 | 240 | class PySchema(Schema): 241 | query: Optional[Query] 242 | 243 | assert str(PySchema) == '''""" 244 | Query() 245 | """ 246 | type Query { 247 | fooA( 248 | a: String! 249 | ): Bar! 250 | } 251 | 252 | """ 253 | Bar(a: Union[ForwardRef('Foo'), NoneType]) 254 | """ 255 | type Bar { 256 | a: Foo 257 | } 258 | 259 | """ 260 | Foo(a: Union[ForwardRef('Bar'), NoneType]) 261 | """ 262 | type Foo { 263 | a: Bar 264 | } 265 | 266 | """ 267 | PySchema(query: Union[tests.test_definition.test_circular_definition..Query, NoneType]) 268 | """ 269 | schema { 270 | query: Query 271 | }''' 272 | 273 | 274 | def test_interface(): 275 | class Foo(Interface): 276 | a: str 277 | 278 | class Baz(Interface): 279 | b: int 280 | 281 | class Bar(Object, Foo, Baz): 282 | pass 283 | 284 | class Query(Object): 285 | @field 286 | def get_foo(self, a: str) -> Foo: 287 | return Bar(a='test') 288 | 289 | class PySchema(Schema): 290 | query: Optional[Query] 291 | 292 | assert str(PySchema) == '''""" 293 | Query() 294 | """ 295 | type Query { 296 | getFoo( 297 | a: String! 298 | ): Foo! 299 | } 300 | 301 | """ 302 | Foo(a: str) 303 | """ 304 | interface Foo { 305 | a: String! 306 | } 307 | 308 | """ 309 | Bar(b: int, a: str) 310 | """ 311 | type Bar implements Foo & Baz { 312 | b: Int! 313 | a: String! 314 | } 315 | 316 | """ 317 | PySchema(query: Union[tests.test_definition.test_interface..Query, NoneType]) 318 | """ 319 | schema { 320 | query: Query 321 | }''' 322 | 323 | 324 | def test_field_name_case(): 325 | class FooInput(Input): 326 | snake_case: str 327 | camelCase: str 328 | 329 | class Foo(Object): 330 | snake_case: str 331 | camelCase: str 332 | 333 | class Query(Object): 334 | @field 335 | def get_foo(self, foo: FooInput) -> Foo: 336 | return Foo(snake_case=foo.snake_case, 337 | camelCase=foo.camelCase) 338 | 339 | class PySchema(Schema): 340 | query: Optional[Query] 341 | 342 | assert str(PySchema) == '''""" 343 | Query() 344 | """ 345 | type Query { 346 | getFoo( 347 | foo: FooInput! 348 | ): Foo! 349 | } 350 | 351 | """ 352 | Foo(snake_case: str, camelCase: str) 353 | """ 354 | type Foo { 355 | snakeCase: String! 356 | camelCase: String! 357 | } 358 | 359 | """ 360 | FooInput(snake_case: str, camelCase: str) 361 | """ 362 | input FooInput { 363 | snakeCase: String! 364 | camelCase: String! 365 | } 366 | 367 | """ 368 | PySchema(query: Union[tests.test_definition.test_field_name_case..Query, NoneType]) 369 | """ 370 | schema { 371 | query: Query 372 | }''' 373 | -------------------------------------------------------------------------------- /tests/test_execution.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing import Optional 3 | from pygraphy.types import ( 4 | Object, 5 | Input, 6 | Schema, 7 | field 8 | ) 9 | from examples.starwars.schema import Schema as StarwarsSchema 10 | from examples.simple_example import Schema as SimpleSchema 11 | from examples.complex_example import Schema as ComplexSchema 12 | 13 | 14 | pytestmark = pytest.mark.asyncio 15 | 16 | 17 | async def test_starwars_query(): 18 | query = """ 19 | query FetchLukeQuery { 20 | human(id: "1000") { 21 | name 22 | } 23 | } 24 | """ 25 | assert await StarwarsSchema.execute(query, serialize=True) == \ 26 | r'{"errors": null, "data": {"human": {"name": "foo"}}}' 27 | 28 | 29 | async def test_simple_query(): 30 | query = """ 31 | query something { 32 | patron { 33 | id 34 | name 35 | age 36 | } 37 | } 38 | """ 39 | 40 | assert await SimpleSchema.execute(query, serialize=True) == \ 41 | r'{"errors": null, "data": {"patron": {"id": "1", "name": "Syrus", "age": 27}}}' 42 | 43 | 44 | query = """ 45 | query { 46 | patrons(ids: [1, 2, 3]) { 47 | id 48 | name 49 | age 50 | } 51 | } 52 | """ 53 | 54 | assert await SimpleSchema.execute(query, serialize=True) == \ 55 | r'{"errors": null, "data": {"patrons": [{"id": "1", "name": "Syrus", "age": 27}, {"id": "2", "name": "Syrus", "age": 27}, {"id": "3", "name": "Syrus", "age": 27}]}}' 56 | 57 | 58 | async def test_alias_field(): 59 | query = """ 60 | query something { 61 | user: patron { 62 | id 63 | firstName: name 64 | age 65 | } 66 | } 67 | """ 68 | 69 | assert await SimpleSchema.execute(query) == { 70 | 'data': { 71 | 'user': { 72 | 'age': 27, 'firstName': 'Syrus', 'id': '1' 73 | } 74 | }, 'errors': None 75 | } 76 | 77 | 78 | async def test_complex_query(): 79 | query = """ 80 | query something{ 81 | address(geo: {lat:32.2, lng:12}) { 82 | latlng 83 | } 84 | } 85 | """ 86 | 87 | assert await ComplexSchema.execute(query, serialize=True) == \ 88 | r'{"errors": null, "data": {"address": {"latlng": "(32.2,12)"}}}' 89 | 90 | 91 | async def test_complex_mutation(): 92 | mutation = """ 93 | mutation addAddress{ 94 | createAddress(geo: {lat:32.2, lng:12}) { 95 | latlng 96 | foobar { 97 | ... on Bar { 98 | b 99 | } 100 | } 101 | } 102 | } 103 | """ 104 | 105 | assert await ComplexSchema.execute(mutation, serialize=True) == \ 106 | r'{"errors": null, "data": {"createAddress": {"latlng": "(32.2,12)", "foobar": [{}, {}, {}, {}, {}]}}}' 107 | 108 | mutation = """ 109 | mutation addAddress{ 110 | createAddress(geo: {lat:32.2, lng:12}) { 111 | latlng 112 | foobar { 113 | ... on Foo { 114 | a 115 | } 116 | } 117 | } 118 | } 119 | """ 120 | 121 | assert await ComplexSchema.execute(mutation, serialize=True) == \ 122 | r'{"errors": null, "data": {"createAddress": {"latlng": "(32.2,12)", "foobar": [{"a": "test"}, {"a": "test"}, {"a": "test"}, {"a": "test"}, {"a": "test"}]}}}' 123 | 124 | 125 | async def test_raise_error(): 126 | query = """ 127 | query test { 128 | exception(content: "test") 129 | } 130 | """ 131 | 132 | assert await SimpleSchema.execute(query, serialize=True) == \ 133 | '{"errors": [{"message": "test", "locations": [{"line": 3, "column": 13}], "path": ["exception"]}], "data": null}' 134 | 135 | 136 | async def test_variables(): 137 | query = """ 138 | query something($geo: GeoInput) { 139 | address(geo: $geo) { 140 | latlng 141 | } 142 | } 143 | """ 144 | 145 | assert await ComplexSchema.execute(query, serialize=True, variables={"geo": {"lat":32.2, "lng":12}}) == \ 146 | r'{"errors": null, "data": {"address": {"latlng": "(32.2,12)"}}}' 147 | 148 | query = """ 149 | query something($patron: [int]) { 150 | patrons(ids: $patron) { 151 | id 152 | name 153 | age 154 | } 155 | } 156 | """ 157 | assert await SimpleSchema.execute(query, serialize=True, variables={"patron": [1, 2, 3]}) == \ 158 | r'{"errors": null, "data": {"patrons": [{"id": "1", "name": "Syrus", "age": 27}, {"id": "2", "name": "Syrus", "age": 27}, {"id": "3", "name": "Syrus", "age": 27}]}}' 159 | 160 | 161 | async def test_field_name_case(): 162 | class FooInput(Input): 163 | snake_case: str 164 | camelCase: str 165 | 166 | class Foo(Object): 167 | snake_case: str 168 | camelCase: str 169 | 170 | class Query(Object): 171 | @field 172 | def get_foo(self, foo: FooInput) -> Foo: 173 | return Foo(snake_case=foo.snake_case, 174 | camelCase=foo.camelCase) 175 | 176 | class PySchema(Schema): 177 | query: Optional[Query] 178 | 179 | query = """ 180 | query something($foo: FooInput) { 181 | get_foo (foo: { 182 | snakeCase: "sth" 183 | camelCase: "sth" 184 | }) { 185 | snakeCase 186 | camelCase 187 | } 188 | } 189 | """ 190 | assert await PySchema.execute(query, serialize=True) == \ 191 | r'{"errors": null, "data": {"get_foo": {"snakeCase": "sth", "camelCase": "sth"}}}' 192 | 193 | 194 | async def test_field_name_case_with_vars(): 195 | 196 | class FooInput(Input): 197 | snake_case: str 198 | camelCase: str 199 | 200 | class Foo(Object): 201 | snake_case: str 202 | camelCase: str 203 | 204 | class Query(Object): 205 | @field 206 | def get_foo(self, foo: FooInput) -> Foo: 207 | return Foo(snake_case=foo.snake_case, 208 | camelCase=foo.camelCase) 209 | 210 | class PySchema(Schema): 211 | query: Optional[Query] 212 | 213 | query = """ 214 | query something($foo: FooInput) { 215 | get_foo (foo: $foo) { 216 | snake_case 217 | camelCase 218 | } 219 | } 220 | """ 221 | assert await PySchema.execute(query, serialize=True, variables={"foo": {"snakeCase":"sth", "camelCase":"sth"}}) == \ 222 | r'{"errors": null, "data": {"get_foo": {"snake_case": "sth", "camelCase": "sth"}}}' 223 | -------------------------------------------------------------------------------- /tests/test_introspection.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from examples.starwars.schema import Schema 4 | from examples.complex_example import Schema as ComplexSchema 5 | 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | async def test_introspection(): 11 | query = """ 12 | query IntrospectionQuery { 13 | __schema { 14 | queryType { 15 | name 16 | } 17 | mutationType { 18 | name 19 | } 20 | subscriptionType { 21 | name 22 | } 23 | types { 24 | ...FullType 25 | } 26 | directives { 27 | name 28 | description 29 | locations 30 | args { 31 | ...InputValue 32 | } 33 | } 34 | } 35 | } 36 | 37 | fragment FullType on __Type { 38 | kind 39 | name 40 | description 41 | fields(includeDeprecated: true) { 42 | name 43 | description 44 | args { 45 | ...InputValue 46 | } 47 | type { 48 | ...TypeRef 49 | } 50 | isDeprecated 51 | deprecationReason 52 | } 53 | inputFields { 54 | ...InputValue 55 | } 56 | interfaces { 57 | ...TypeRef 58 | } 59 | enumValues(includeDeprecated: true) { 60 | name 61 | description 62 | isDeprecated 63 | deprecationReason 64 | } 65 | possibleTypes { 66 | ...TypeRef 67 | } 68 | } 69 | 70 | fragment InputValue on __InputValue { 71 | name 72 | description 73 | type { 74 | ...TypeRef 75 | } 76 | defaultValue 77 | } 78 | 79 | fragment TypeRef on __Type { 80 | kind 81 | name 82 | ofType { 83 | kind 84 | name 85 | ofType { 86 | kind 87 | name 88 | ofType { 89 | kind 90 | name 91 | ofType { 92 | kind 93 | name 94 | ofType { 95 | kind 96 | name 97 | ofType { 98 | kind 99 | name 100 | ofType { 101 | kind 102 | name 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | }""" 111 | 112 | path = '/'.join(os.path.abspath(__file__).split('/')[:-1]) 113 | with open(f'{path}/introspection_result', 'r') as f: 114 | result1 = await Schema.execute(query, serialize=True) 115 | assert result1 == f.readline()[:-1] 116 | result2 = await ComplexSchema.execute(query, serialize=True) 117 | assert result2 == f.readline()[:-1] 118 | -------------------------------------------------------------------------------- /tests/test_recursive_def.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import pygraphy 3 | from typing import Optional 4 | 5 | 6 | class WhereInput(pygraphy.Input): 7 | _and: Optional[WhereInput] = None 8 | 9 | 10 | class Query(pygraphy.Query): 11 | 12 | @pygraphy.field 13 | def foo(self, arg: WhereInput) -> int: 14 | return 0 15 | 16 | 17 | class Schema(pygraphy.Schema): 18 | query: Optional[Query] 19 | 20 | 21 | def test_recursive_definition(): 22 | print(str(Schema)) 23 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from pygraphy.utils import patch_indents, is_union, is_optional, is_list 3 | 4 | 5 | def test_patch_indents(): 6 | assert patch_indents('test {\ntest\n}') == 'test {\ntest\n}' 7 | assert patch_indents('test {\ntest\n}', indent=1) == ' test {\n test\n }' 8 | 9 | 10 | def test_is_union(): 11 | assert is_union(typing.Optional[str]) is True 12 | assert is_union(typing.Union[str, int, None]) is True 13 | assert is_union(str) is False 14 | 15 | 16 | def test_is_optional(): 17 | assert is_optional(typing.Optional[str]) is True 18 | assert is_optional(typing.Union[str, None]) is True 19 | assert is_optional(typing.Union[str, int, None]) is False 20 | 21 | 22 | def test_is_list(): 23 | assert is_list(typing.List[str]) is True 24 | assert is_list(typing.Union[str, int, None]) is False 25 | -------------------------------------------------------------------------------- /tests/test_view.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from starlette.testclient import TestClient 4 | 5 | 6 | @pytest.fixture() 7 | def client(): 8 | from examples.starwars.schema import app 9 | return TestClient(app) 10 | 11 | 12 | def test_get_playground(client): 13 | response = client.get('/') 14 | assert response.status_code == 200 15 | 16 | 17 | def test_request(client): 18 | content = { 19 | 'operationName': None, 20 | 'query': "{\n hero(episode: JEDI) {\n id\n name\n }\n}\n", 21 | "variables": {} 22 | } 23 | response = client.post( 24 | '/', data=json.dumps(content), headers={'content-type': 'application/json'}) 25 | assert response.status_code == 200 26 | 27 | content = "{\n hero(episode: JEDI) {\n id\n name\n }\n}\n" 28 | response = client.post( 29 | '/', data=content, headers={'content-type': 'application/graphql'}) 30 | assert response.status_code == 200 31 | 32 | 33 | def test_error(client): 34 | content = { 35 | 'operationName': None, 36 | 'query': "{\n hero(episode: JEDI) {\n id\n name\n }\n}\n", 37 | "variables": {} 38 | } 39 | response = client.post( 40 | '/', data=json.dumps(content), headers={'content-type': 'application/text'}) 41 | assert response.status_code == 415 42 | -------------------------------------------------------------------------------- /tests/test_websocket.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import pytest 4 | from starlette.testclient import TestClient 5 | 6 | 7 | @pytest.fixture() 8 | def client(): 9 | from examples.starwars.schema import app 10 | return TestClient(app, raise_server_exceptions=False) 11 | 12 | 13 | def test_subscription(client): 14 | with client.websocket_connect('/ws') as websocket: 15 | query = ''' 16 | subscription test { 17 | beat { 18 | beat 19 | foo(arg: 2) 20 | } 21 | } 22 | ''' 23 | data = {'type': 'start', 'id': 1, 'payload': {'query': query, 'variables': {}}} 24 | websocket.send_json(data) 25 | start = 0 26 | for i in range(10): 27 | data = websocket.receive_json() 28 | assert data == {'type': 'data', 'id': 1, 'payload': { 29 | 'data': {'beat': {'beat': start, 'foo': start * 2}}, 'errors': None} 30 | } 31 | start += 1 32 | 33 | 34 | def test_query(client): 35 | with client.websocket_connect('/ws') as websocket: 36 | query = """query IntrospectionQuery { 37 | __schema { 38 | queryType { 39 | name 40 | } 41 | mutationType { 42 | name 43 | } 44 | subscriptionType { 45 | name 46 | } 47 | types { 48 | ...FullType 49 | } 50 | directives { 51 | name 52 | description 53 | locations 54 | args { 55 | ...InputValue 56 | } 57 | } 58 | } 59 | } 60 | 61 | fragment FullType on __Type { 62 | kind 63 | name 64 | description 65 | fields(includeDeprecated: true) { 66 | name 67 | description 68 | args { 69 | ...InputValue 70 | } 71 | type { 72 | ...TypeRef 73 | } 74 | isDeprecated 75 | deprecationReason 76 | } 77 | inputFields { 78 | ...InputValue 79 | } 80 | interfaces { 81 | ...TypeRef 82 | } 83 | enumValues(includeDeprecated: true) { 84 | name 85 | description 86 | isDeprecated 87 | deprecationReason 88 | } 89 | possibleTypes { 90 | ...TypeRef 91 | } 92 | } 93 | 94 | fragment InputValue on __InputValue { 95 | name 96 | description 97 | type { 98 | ...TypeRef 99 | } 100 | defaultValue 101 | } 102 | 103 | fragment TypeRef on __Type { 104 | kind 105 | name 106 | ofType { 107 | kind 108 | name 109 | ofType { 110 | kind 111 | name 112 | ofType { 113 | kind 114 | name 115 | ofType { 116 | kind 117 | name 118 | ofType { 119 | kind 120 | name 121 | ofType { 122 | kind 123 | name 124 | ofType { 125 | kind 126 | name 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | }""" 135 | data = {'type': 'start', 'id': 2, 'payload': {'query': query, 'variables': {}}} 136 | websocket.send_text(json.dumps(data)) 137 | data = websocket.receive_text() 138 | path = '/'.join(os.path.abspath(__file__).split('/')[:-1]) 139 | with open(f'{path}/subscription_introspection', 'r') as f: 140 | assert data == f.read()[:-1] 141 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | 2 | [tox] 3 | envlist = flake8, py37, coverage 4 | extras = web 5 | 6 | [testenv] 7 | passenv = * 8 | changedir = 9 | tests 10 | 11 | commands = 12 | py.test 13 | 14 | deps = 15 | starlette 16 | requests 17 | uvicorn 18 | pytest 19 | coverage 20 | pytest-cov 21 | pytest-asyncio 22 | 23 | [testenv:flake8] 24 | deps = flake8 25 | commands = flake8 pygraphy --ignore E501,W503 26 | 27 | [testenv:coverage] 28 | commands = 29 | py.test --cov=pygraphy 30 | 31 | [coverage:run] 32 | branch = True 33 | --------------------------------------------------------------------------------