├── .gitignore ├── .travis.yml ├── README.md ├── graphql_parser ├── __init__.py ├── parser.py └── transform.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── graphql_parser_tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | 5 | install: "pip install -r requirements.txt" 6 | 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-parser 2 | 3 | [![Build Status](https://travis-ci.org/tryolabs/graphql-parser.svg?branch=master)](https://travis-ci.org/tryolabs/graphql-parser) 4 | 5 | This is a Python parser for [React's][react] [GraphQL][graphql]. 6 | 7 | Lacking a specification, the parser was built to parse code along the lines of 8 | examples and other implementations of GraphQL. 9 | 10 | # Usage 11 | 12 | ```python 13 | from graphql_parser import parse 14 | 15 | QUERY = '''{ 16 | user(1) { 17 | name, 18 | email, 19 | profile_pic.size(64) { 20 | date_added 21 | } 22 | } 23 | } 24 | ''' 25 | 26 | parse(QUERY) 27 | ``` 28 | 29 | Produces: 30 | 31 | ```python 32 | { 33 | 'type': 'block', 34 | 'children': [ 35 | { 36 | 'type': 'call', 37 | 'chain': ['user'], 38 | 'arguments': ['1'], 39 | 'body': { 40 | 'type': 'block', 41 | 'children': [ 42 | 'name', 43 | 'email', 44 | { 45 | 'type': 'call', 46 | 'chain': ['profile_pic', 'size'], 47 | 'arguments': ['64'], 48 | 'body': { 49 | 'type': 'block', 50 | 'children': ['date_added'] 51 | }, 52 | } 53 | ] 54 | } 55 | } 56 | ] 57 | } 58 | ``` 59 | 60 | # License 61 | 62 | Copyright (c) 2015 [Tryolabs][tryo] SRL. 63 | 64 | Released under the MIT license. 65 | 66 | [react]: http://facebook.github.io/react/ 67 | [graphql]: https://facebook.github.io/react/blog/2015/05/01/graphql-introduction.html 68 | [tryo]: http://tryolabs.com/ 69 | -------------------------------------------------------------------------------- /graphql_parser/__init__.py: -------------------------------------------------------------------------------- 1 | import pypeg2 2 | 3 | from graphql_parser.parser import Block 4 | from graphql_parser.transform import transform_block 5 | 6 | 7 | def parse(string): 8 | """Parse a GraphQL string into a dictionary.""" 9 | return transform_block(pypeg2.parse(string, Block)) 10 | -------------------------------------------------------------------------------- /graphql_parser/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module implements the parser. 4 | 5 | :copyright: (C) 2015 Tryolabs SRL 6 | :license: MIT 7 | """ 8 | 9 | import re 10 | from pypeg2 import name, csl, List 11 | 12 | 13 | class Field(): 14 | """A field name in a query.""" 15 | grammar = name() 16 | 17 | 18 | number = re.compile(r'[+-]?(\d)+') 19 | 20 | 21 | class Arguments(List): 22 | """Arguments to a call.""" 23 | grammar = csl(number, separator=',') 24 | 25 | 26 | class CallList(List): 27 | grammar = csl(Field, separator='.') 28 | 29 | 30 | class Call(List): 31 | """A function call.""" 32 | 33 | def names(self): 34 | return self[0] 35 | 36 | def arguments(self): 37 | return self[1] 38 | 39 | def body(self): 40 | return self[2] 41 | 42 | 43 | class Block(List): 44 | """A curly brace delimited block.""" 45 | grammar = '{', csl([Call, Field], separator=','), '}' 46 | 47 | 48 | Call.grammar = CallList, '(', Arguments, ')', Block 49 | -------------------------------------------------------------------------------- /graphql_parser/transform.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Transform the result of the parser into a dictionary. 4 | 5 | :copyright: (C) 2015 Tryolabs SRL 6 | :license: MIT 7 | """ 8 | 9 | from graphql_parser.parser import Call 10 | 11 | 12 | def transform_block(block): 13 | """Transform a block and its children into a dictionary.""" 14 | return { 15 | 'type': 'block', 16 | 'children': [transform_child(child) for child in block] 17 | } 18 | 19 | 20 | def transform_child(child): 21 | """Transform an element of a block.""" 22 | # Is it a field name or a call? 23 | if isinstance(child, Call): 24 | return transform_call(child) 25 | else: 26 | return str(child.name) 27 | 28 | 29 | def transform_call(call): 30 | """Transform a call into a dictionary.""" 31 | return { 32 | 'type': 'call', 33 | 'chain': [str(fn.name) for fn in call.names()], 34 | 'arguments': [str(arg) for arg in call.arguments()], 35 | 'body': transform_block(call.body()) 36 | } 37 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyPEG2==2.15.0 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | from setuptools import setup 4 | except ImportError: 5 | from distutils.core import setup 6 | 7 | config = { 8 | 'description': 'GraphQL parser for Python', 9 | 'author': 'Tryolabs', 10 | 'url': 'https://github.com/tryolabs/graphql-parser', 11 | 'author_email': 'fernando@tryolabs.com.', 12 | 'version': '0.1', 13 | 'install_requires': ['pypeg2'], 14 | 'packages': ['graphql_parser'], 15 | 'scripts': [], 16 | 'name': 'graphql-parser' 17 | } 18 | 19 | setup(**config) 20 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/graphql-parser/e528b462a978af1ba8d4dcddf90bad5bf757258a/tests/__init__.py -------------------------------------------------------------------------------- /tests/graphql_parser_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from nose.tools import * 3 | from pypeg2 import parse 4 | 5 | import graphql_parser as gqlp 6 | 7 | from graphql_parser.parser import number, Arguments, Field, Block, Call 8 | 9 | 10 | def test_number(): 11 | assert parse('1', number) == '1' 12 | assert parse('1234', number) == '1234' 13 | assert parse('-34', number) == '-34' 14 | 15 | 16 | def test_arguments(): 17 | assert parse('1,2,3', Arguments) == ['1', '2', '3'] 18 | assert parse('1,2 ,3 ', Arguments) == ['1', '2', '3'] 19 | 20 | 21 | def test_field(): 22 | assert parse('test', Field).name == 'test' 23 | 24 | 25 | def test_block(): 26 | assert [f.name for f in parse('{a,b,c}', Block)] == ['a', 'b', 'c'] 27 | 28 | 29 | def test_call(): 30 | call = parse('test(3, 4) { a, b, c }', Call) 31 | assert [f.name for f in call.names()] == ['test'] 32 | assert call.arguments() == ['3', '4'] 33 | assert [f.name for f in call.body()] == ['a', 'b', 'c'] 34 | 35 | 36 | def test_multi_call(): 37 | call = parse('test.method(1) { a }', Call) 38 | assert [f.name for f in call.names()] == ['test', 'method'] 39 | assert call.arguments() == ['1'] 40 | assert [f.name for f in call.body()] == ['a'] 41 | 42 | 43 | def test_all(): 44 | block = parse('{a, test.method(1) { b }, c}', Block) 45 | 46 | 47 | def test_transform(): 48 | assert gqlp.parse('{ a, b, c }') == { 49 | 'type': 'block', 50 | 'children': 'a b c'.split() 51 | } 52 | --------------------------------------------------------------------------------