├── .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 | [](https://travis-ci.org/ethe/pygraphy)
5 | [](https://codecov.io/gh/ethe/pygraphy)
6 | [](https://pypi.org/project/pygraphy/)
7 | [](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 | [](https://travis-ci.org/ethe/pygraphy)
6 | [](https://codecov.io/gh/ethe/pygraphy)
7 | [](https://pypi.org/project/pygraphy/)
8 | [](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 | 
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 |
--------------------------------------------------------------------------------