├── requirements.txt ├── codegen ├── __init__.py ├── src │ ├── __init__.py │ ├── templates │ │ ├── enum_template.py │ │ ├── scalar_template.py │ │ ├── type_refs_template.py │ │ ├── query_template.py │ │ ├── mutation_template.py │ │ ├── simple_type_template.py │ │ └── type_template.py │ ├── consts.py │ ├── priority.py │ ├── enums.py │ ├── sp_schema.py │ ├── utils.py │ ├── printer.py │ └── base_class.py ├── requirements.txt ├── generator.py ├── network.py ├── query_presets.py ├── __main__.py └── README.MD ├── pygqlmap ├── requirements.txt ├── src │ ├── __init__.py │ ├── enums.py │ ├── consts.py │ ├── arg_builtin.py │ ├── components.py │ ├── utils.py │ ├── builder.py │ └── base.py ├── config.ini ├── __init__.py ├── gql_types.py ├── enums.py ├── helper.py ├── gql_operations.py ├── network.py └── components.py ├── tests ├── __init__.py ├── cli_input │ └── rapid_api │ │ ├── downloaderArgs.json │ │ └── generatorArgs.json ├── output │ ├── re │ │ ├── type_refs.py │ │ ├── scalars.py │ │ ├── enums.py │ │ └── gql_simple_types.py │ ├── rapidapi │ │ ├── type_refs.py │ │ └── scalars.py │ ├── re_nodesc │ │ ├── type_refs.py │ │ ├── scalars.py │ │ └── enums.py │ ├── rapidapi_nodesc │ │ ├── type_refs.py │ │ └── scalars.py │ ├── github │ │ ├── wa inconsistence.txt │ │ ├── subscriptions.py │ │ ├── type_refs.py │ │ └── scalars.py │ ├── gdbc_nodesc │ │ ├── scalars.py │ │ ├── type_refs.py │ │ ├── gql_simple_types.py │ │ ├── enums.py │ │ ├── queries.py │ │ └── gql_types.py │ ├── github_nodesc │ │ ├── wa inconsistence.txt │ │ ├── scalars.py │ │ └── type_refs.py │ └── gdbc │ │ ├── type_refs.py │ │ ├── scalars.py │ │ ├── enums.py │ │ └── gql_simple_types.py ├── help_unittest.py ├── consts.py ├── test_codegen.py ├── gdbc_unittest.py ├── utils.py ├── tstquery │ ├── simple_obj_test.py │ ├── connobj_test.py │ ├── simple_obj_args_literal_test.py │ ├── simple_obj_viewchange_test.py │ ├── simple_obj_args_vars_test.py │ ├── connobj_args_literal_test.py │ ├── connobj_args_vars_test.py │ ├── connobj_viewchange_test.py │ ├── manual_query_fragment_test.py │ └── complex_obj_test.py ├── test_cli.py ├── tstmutation │ ├── mutation_test.py │ └── manual_mutation_test.py ├── re_unittest.py ├── README.MD └── spot_test.py ├── .github ├── FUNDING.yml └── workflows │ ├── test-codegen.yml │ ├── test-map.yml │ ├── python-package.yml │ └── test-cli.yml ├── MANIFEST.in ├── .gitattributes ├── docs ├── cli_args_nutshell.png ├── pygqlmap - UML Design.pdf ├── codegen - Parsed Schema Structure.pdf └── codegen - Schema transformation overview.pdf ├── pytest.ini ├── cli_args.json ├── setup.py ├── pyproject.toml ├── LICENSE ├── RELEASE_NOTES.MD ├── .gitignore └── README.MD /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /codegen/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /codegen/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /codegen/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pygqlmap/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pygqlmap/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import tests.utils -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dapalex 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # MANIFEST.in 2 | 3 | include pygqlmap/config.ini -------------------------------------------------------------------------------- /codegen/src/templates/enum_template.py: -------------------------------------------------------------------------------- 1 | from enum import Enum -------------------------------------------------------------------------------- /codegen/src/templates/scalar_template.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * -------------------------------------------------------------------------------- /pygqlmap/config.ini: -------------------------------------------------------------------------------- 1 | [MAPCONFIG] 2 | recursionDepth = 0 3 | forceStack = 0 -------------------------------------------------------------------------------- /pygqlmap/__init__.py: -------------------------------------------------------------------------------- 1 | from .gql_operations import GQLQuery, GQLMutation 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /docs/cli_args_nutshell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dapalex/py-graphql-mapper/HEAD/docs/cli_args_nutshell.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli=true 3 | log_level=DEBUG 4 | markers = 5 | webtest: mark a test as a webtest. -------------------------------------------------------------------------------- /tests/cli_input/rapid_api/downloaderArgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaFile": "./cmd_output/rapidapi/schema.json" 3 | } -------------------------------------------------------------------------------- /tests/cli_input/rapid_api/generatorArgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaFile": "./tests/cmd_output/rapidapi/schema.json" 3 | } -------------------------------------------------------------------------------- /docs/pygqlmap - UML Design.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dapalex/py-graphql-mapper/HEAD/docs/pygqlmap - UML Design.pdf -------------------------------------------------------------------------------- /docs/codegen - Parsed Schema Structure.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dapalex/py-graphql-mapper/HEAD/docs/codegen - Parsed Schema Structure.pdf -------------------------------------------------------------------------------- /tests/output/re/type_refs.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | 5 | -------------------------------------------------------------------------------- /docs/codegen - Schema transformation overview.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dapalex/py-graphql-mapper/HEAD/docs/codegen - Schema transformation overview.pdf -------------------------------------------------------------------------------- /tests/output/rapidapi/type_refs.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | 5 | -------------------------------------------------------------------------------- /tests/output/re_nodesc/type_refs.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | 5 | -------------------------------------------------------------------------------- /codegen/src/templates/type_refs_template.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | -------------------------------------------------------------------------------- /tests/output/rapidapi_nodesc/type_refs.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | 5 | -------------------------------------------------------------------------------- /tests/output/github/wa inconsistence.txt: -------------------------------------------------------------------------------- 1 | from 2 | 3 | _______ 4 | data: Query 5 | _______ 6 | 7 | to 8 | 9 | _______ 10 | data: None 11 | _______ 12 | 13 | for in consistence in GraphQL schema -------------------------------------------------------------------------------- /tests/output/gdbc_nodesc/scalars.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * 2 | 3 | Boolean = bool 4 | 5 | Float = float 6 | 7 | ID = ID 8 | 9 | Int = int 10 | 11 | String = str 12 | 13 | distance = float 14 | -------------------------------------------------------------------------------- /tests/output/github_nodesc/wa inconsistence.txt: -------------------------------------------------------------------------------- 1 | from 2 | 3 | _______ 4 | type: Query 5 | _______ 6 | 7 | to 8 | 9 | _______ 10 | type: None 11 | _______ 12 | 13 | in queries.py 14 | for in consistence in GraphQL schema -------------------------------------------------------------------------------- /codegen/src/templates/query_template.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from pygqlmap import GQLQuery 3 | from .gql_types import * 4 | from .gql_simple_types import * 5 | from .enums import * 6 | from .scalars import * 7 | from .type_refs import * -------------------------------------------------------------------------------- /pygqlmap/gql_types.py: -------------------------------------------------------------------------------- 1 | class ID(str): pass 2 | class NonNull_int(int): pass 3 | class NonNull_float(float): pass 4 | class NonNull_bool(int): pass 5 | class NonNull_str(str): pass 6 | class NonNull_ID(ID): pass 7 | class NonNull_list(list): pass 8 | -------------------------------------------------------------------------------- /codegen/src/templates/mutation_template.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from pygqlmap import GQLMutation 3 | from .gql_types import * 4 | from .gql_simple_types import * 5 | from .enums import * 6 | from .scalars import * 7 | from .type_refs import * 8 | -------------------------------------------------------------------------------- /pygqlmap/enums.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | 4 | class ArgType(Enum): 5 | LITERAL_VALUES = 0 6 | VARIABLES = 1 7 | 8 | class OperationType(Enum): 9 | QUERY = "query" 10 | MUTATION = "mutation" 11 | GENERIC_TYPE = "generic" 12 | -------------------------------------------------------------------------------- /tests/output/gdbc/type_refs.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | 5 | 6 | RegionPopulatedPlacesConnection = TypeVar('RegionPopulatedPlacesConnection', bound=GQLObject) 7 | -------------------------------------------------------------------------------- /tests/output/gdbc_nodesc/type_refs.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | 5 | 6 | RegionPopulatedPlacesConnection = TypeVar('RegionPopulatedPlacesConnection', bound=GQLObject) 7 | -------------------------------------------------------------------------------- /codegen/src/templates/simple_type_template.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, List 2 | from pygqlmap.components import GQLArgsSet, GQLObject 3 | from pygqlmap.gql_types import * 4 | from pygqlmap.src.arg_builtin import * 5 | from .enums import * 6 | from .scalars import * 7 | from .type_refs import * -------------------------------------------------------------------------------- /cli_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiURL": "https://mygraphqlapi.com/v2", 3 | "httpHeaders": { 4 | "Authorization": "bearer abcdef12345678", 5 | "additionalHeader-content-type": "application/json" 6 | }, 7 | "schemaFile": "", 8 | "addDescToGeneratedFiles": "True" 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/output/rapidapi_nodesc/scalars.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * 2 | 3 | String = str 4 | 5 | Boolean = bool 6 | 7 | ID = ID 8 | 9 | Int = int 10 | 11 | DateTime = str 12 | 13 | Float = float 14 | 15 | Any = str 16 | 17 | Upload = str 18 | 19 | JSONObject = str 20 | 21 | name = str 22 | -------------------------------------------------------------------------------- /pygqlmap/src/enums.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | 4 | class BuildingType(Enum): 5 | STANDARD = 0, #just keeps the hidden fields present with value None 6 | ALTERCLASS = 1, #Standard + modify __dict__/__get__ (__set__ __delete__??) to not allow working with it 7 | CREATENEWCLASS = 2 #Needs to create a new type -------------------------------------------------------------------------------- /codegen/src/templates/type_template.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, Union, List 2 | from pygqlmap.components import GQLArgsSet, GQLObject 3 | from pygqlmap.gql_types import * 4 | from pygqlmap.src.arg_builtin import * 5 | from typing import NewType 6 | from .gql_simple_types import * 7 | from .enums import * 8 | from .scalars import * 9 | from .type_refs import * -------------------------------------------------------------------------------- /tests/output/re_nodesc/scalars.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * 2 | 3 | String = str 4 | 5 | ID = ID 6 | 7 | Boolean = bool 8 | 9 | DateTime = str 10 | 11 | Float = float 12 | 13 | Int = int 14 | 15 | Date = str 16 | 17 | BigInt = str 18 | 19 | Decimal = str 20 | 21 | JSONString = str 22 | 23 | Upload = str 24 | 25 | GenericScalar = str 26 | -------------------------------------------------------------------------------- /tests/help_unittest.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging as logger 4 | 5 | def run_generator_cmd_help(): 6 | logger.debug('\nRunning run_generator_cmd_help...') 7 | command = "CodeGenerator -h" #command to be executed 8 | logger.debug("Launching: " + command) 9 | 10 | ret = os.system(command) 11 | logger.debug("End of run_generator_cmd_help") 12 | -------------------------------------------------------------------------------- /tests/output/gdbc/scalars.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * 2 | 3 | Boolean = bool ##Built-in Boolean 4 | 5 | Float = float ##Built-in Float 6 | 7 | ID = ID ##Built-in ID 8 | 9 | Int = int ##Built-in Int 10 | 11 | String = str ##Built-in String 12 | 13 | distance = float ##The distance result from some location-based query - This field has two forms: - - As a property (e.g., place.distance), returns the distance as part of a query returning places sorted by distance. - - As a function (e.g., place.distance(toPlaceId), returns the distance to the specified place. 14 | -------------------------------------------------------------------------------- /pygqlmap/src/consts.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import ID 2 | 3 | PRIMITIVES = (int, str, bool, float, type(None)) 4 | 5 | GQL_BUILTIN = (int, str, bool, float, type(None), ID) 6 | 7 | STRING_PRIMITIVES = ('int', 'str', 'bool', 'float', 'NoneType') 8 | 9 | STRING_GQL_BUILTIN = ('int', 'str', 'bool', 'float', 'None', 'ID') 10 | 11 | STRING_GQLLIST_BUILTIN = ('int', 'str', 'bool', 'float', 'None', 'ID', 'Any') 12 | 13 | COMMA_CONCAT = ', ' 14 | 15 | ARGS_DECLARE = '_args' 16 | 17 | NON_NULL_PREFIX = 'NonNull_' 18 | 19 | GQLLIST_PREFIX = 'list_' 20 | 21 | ARGUED_SIGNATURE_SUFFIX = '_Field' -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='pygqlmap', 5 | version='1.1.2', 6 | url='https://github.com/dapalex/py-graphql-mapper/tree/1.1.2', 7 | author='Alex Dap', 8 | author_email='shlisi2017@gmail.com', 9 | description='A python library to call GraphQL APIs without using hardcoded strings', 10 | include_package_data=True, 11 | packages=['pygqlmap', 'pygqlmap.src', 'codegen', 'codegen.src', 'codegen.src.templates'], 12 | data_files=[('', ['pygqlmap/config.ini'])], 13 | python_requires='>=3.10', 14 | classifiers=[ 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: MIT License', 17 | 'Programming Language :: Python :: 3', 18 | ], 19 | ) -------------------------------------------------------------------------------- /tests/output/gdbc_nodesc/gql_simple_types.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, List 2 | from pygqlmap.components import GQLArgsSet, GQLObject 3 | from pygqlmap.gql_types import * 4 | from pygqlmap.src.arg_builtin import * 5 | from .enums import * 6 | from .scalars import * 7 | from .type_refs import * 8 | 9 | class PFBPH_distance_Field(ArguedFloat): 10 | class floatArgs(GQLArgsSet, GQLObject): 11 | toPlaceId: ID 12 | distanceUnit: DistanceUnit 13 | 14 | _args: floatArgs 15 | 16 | 17 | 18 | class TimeZone(GQLObject): 19 | id: ID 20 | name: str 21 | rawUtcOffsetHours: int 22 | dateTime: str 23 | time: str 24 | 25 | class Locale(GQLObject): 26 | code: ID 27 | name: str 28 | 29 | class Currency(GQLObject): 30 | countryCodes: list[ID] 31 | code: ID 32 | symbol: str 33 | -------------------------------------------------------------------------------- /tests/output/github/subscriptions.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from pygqlmap import GQLSubscription 3 | from .gql_types import * 4 | from .gql_simple_types import * 5 | from .enums import * 6 | from .scalars import * 7 | # from .type_refs import * 8 | 9 | class repository(GQLSubscription): 10 | """ 11 | repository - Lookup a given repository by the owner and repository name. 12 | 13 | """ 14 | class RepositoryArgs(GQLArgsSet, GQLObject): 15 | """ 16 | owner - The login field of a user or organization 17 | 18 | name - The name of the repository 19 | 20 | followRenames - Follow repository renames. If disabled, a repository referenced by its old name will return an error. 21 | 22 | """ 23 | owner: NonNull_str 24 | name: NonNull_str 25 | followRenames: bool 26 | 27 | _args: RepositoryArgs 28 | 29 | 30 | type: Repository 31 | -------------------------------------------------------------------------------- /tests/consts.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | GDBC_HEADERS = { 4 | "content-type": "application/json", ##it needs to be added for graphQl version 5 | "X-RapidAPI-Key": os.environ['GEODBCITIES_APIKEY'], 6 | "X-RapidAPI-Host": "geodb-cities-graphql.p.rapidapi.com" 7 | } 8 | 9 | GDBC_URL = "https://geodb-cities-graphql.p.rapidapi.com/" 10 | 11 | GITHUB_HEADERS = {"Authorization": os.environ['GH_APIKEY']} 12 | 13 | GITHUB_URL = "https://api.github.com/graphql" 14 | 15 | RAPIDAPI_HEADERS = { 16 | "content-type": "application/json", ##it needs to be added for graphQl version 17 | "X-RapidAPI-Key": os.environ['RAPIDAPI_APIKEY'], 18 | "X-RapidAPI-Host": "graphql-rapidapi-test.p.rapidapi.com" 19 | } 20 | 21 | RAPIDAPI_URL = 'https://graphql-rapidapi-test.p.rapidapi.com/' 22 | 23 | RE_HEADERS = { "Content-Type": "application/json" } 24 | 25 | RE_URL = "https://rippleenergy.com/graphql" 26 | -------------------------------------------------------------------------------- /pygqlmap/helper.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging as logger 3 | import os 4 | from configparser import ConfigParser 5 | 6 | #Read config.ini file 7 | config_object = ConfigParser() 8 | config_object.read(os.path.join(os.path.abspath(os.path.dirname(__file__)), "config.ini"), encoding='UTF-8') 9 | 10 | if not config_object: raise Exception('Config file not') 11 | #Get the USERINFO section 12 | mapConfig = config_object["MAPCONFIG"] 13 | 14 | 15 | 16 | class CustomException(Exception): 17 | """ Custom exception class """ 18 | 19 | def handle_recursive_ex(ex = None, message: str = None): 20 | try: 21 | if ex and CustomException in inspect.getmro(type(ex)): 22 | return ex 23 | else: 24 | logger.error(ex.args[0]) 25 | return CustomException(message + ' - ' + ex.args[0]) 26 | except Exception as hrex: 27 | logger.warning('handle_recursive_ex' + hrex.args[0]) -------------------------------------------------------------------------------- /pygqlmap/gql_operations.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pygqlmap.components import GQLOperation 3 | from pygqlmap.enums import ArgType, OperationType 4 | from pygqlmap.src.gql_init import _query_init, _mutation_init 5 | 6 | class GQLQuery(GQLOperation): 7 | 8 | def __init_subclass__(cls): 9 | cls = dataclass(cls) 10 | cls.__init__ = _query_init 11 | 12 | def __init__(self): 13 | if hasattr(self, 'args'): 14 | self._args = self.args 15 | super().__init__(OperationType.QUERY, self.type, ArgType.LITERAL_VALUES) 16 | 17 | class GQLMutation(GQLOperation): 18 | 19 | def __init_subclass__(cls): 20 | cls = dataclass(cls) 21 | cls.__init__ = _mutation_init 22 | 23 | def __init__(self): 24 | if hasattr(self, 'args'): 25 | self._args = self.args 26 | super().__init__(OperationType.MUTATION, self.type, ArgType.LITERAL_VALUES) 27 | -------------------------------------------------------------------------------- /codegen/src/consts.py: -------------------------------------------------------------------------------- 1 | 2 | from os import path 3 | import pathlib 4 | 5 | CLASS_SIGNATURE = "class %s(GQLObject)" 6 | INTERFACE_SIGNATURE = "class %s(%s)" 7 | ARGUED_CLASS_SIGNATURE = "class %s(%s)" 8 | QUERY_SIGNATURE = "class %s(GQLQuery)" 9 | MUTATION_SIGNATURE = "class %s(GQLMutation)" 10 | ENUM_SIGNATURE = "class %s(Enum)" 11 | SCALAR_SIGNATURE = "%s = %s" 12 | TYPEVAR_SIGNATURE = "%s = TypeVar('%s', bound=%s)" 13 | EMPTY_CLASS_SIGNATURE = "class %s(%s): pass" 14 | GQLLIST_SIGNATURE = "class %s(list, %s): pass" 15 | 16 | NEWTYPE_DECLARATION = "NewType('%s', GQLObject)" 17 | TEMPLATE_FOLDER = str(pathlib.Path(path.dirname(__file__), 'templates').absolute()) 18 | IMPORT_TEMPLATE = "from .%s import %s" 19 | 20 | SCALARS_NAME = 'scalars' 21 | ENUMS_NAME = 'enums' 22 | TYPES_NAME = 'gql_types' 23 | SIMPLE_TYPES_NAME = 'gql_simple_types' 24 | QUERIES_NAME = 'queries' 25 | MUTATIONS_NAME = 'mutations' 26 | TYPE_REFS_NAME = 'type_refs' 27 | 28 | PY_EXTENSION = '.py' 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "py-graphql-mapper" 7 | authors = [ 8 | { name="Alex Dap", email="shlisi2017@gmail.com" }, 9 | ] 10 | readme = "README.md" 11 | keywords = ["python", "graphql", "mapping"] 12 | license = { file = "LICENSE" } 13 | requires-python = ">=3.10" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | dynamic = ["version", "description"] 20 | 21 | [project.scripts] 22 | pgmcodegen = "codegen.__main__:main" 23 | 24 | [project.urls] 25 | "Homepage" = "https://github.com/dapalex/py-graphql-mapper/" 26 | "Source Code" = "https://github.com/dapalex/py-graphql-mapper/tree/1.1.2" 27 | "Bug Tracker" = "https://github.com/dapalex/py-graphql-mapper/issues" 28 | "Release Notes" = "https://github.com/dapalex/py-graphql-mapper/tree/1.1.2/RELEASE_NOTES.MD" -------------------------------------------------------------------------------- /pygqlmap/src/arg_builtin.py: -------------------------------------------------------------------------------- 1 | 2 | from dataclasses import dataclass 3 | from .gql_init import _sub_class_init 4 | 5 | class ArguedBuiltin(): 6 | def __init_subclass__(cls): 7 | cls = dataclass(cls) 8 | cls.__init__ = _sub_class_init 9 | 10 | class ArguedInt(ArguedBuiltin, int): 11 | 12 | def __new__(cls, number = -1): 13 | arguedInt = super().__new__(cls, number) 14 | return arguedInt 15 | 16 | class ArguedStr(ArguedBuiltin, str): 17 | 18 | def __new__(cls, *args, **kw): 19 | return str.__new__(cls, *args, **kw) 20 | 21 | class ArguedBool(ArguedBuiltin): 22 | 23 | self = False 24 | 25 | class ArguedFloat(ArguedBuiltin, float): 26 | 27 | def __init__(self,value = -1): #Constructor like method 28 | self.value = value 29 | 30 | def __add__(self, other): #rounds results to the higher number of decimal places 31 | return round(self.value +other.value, max(self.count_decimal_places(self.value), self.count_decimal_places(other.value))) 32 | -------------------------------------------------------------------------------- /codegen/src/priority.py: -------------------------------------------------------------------------------- 1 | from codegen.src.base_class import SchemaTypeManager 2 | 3 | class PriorElement(): 4 | name: str 5 | schemaType: SchemaTypeManager 6 | codeList: list 7 | 8 | def __init__(self, name, schemaType, codeList): 9 | self.name = name 10 | self.schemaType = schemaType 11 | self.codeList = codeList 12 | 13 | 14 | class ExtractionResults(): 15 | query_classes: dict 16 | queries_enum_class: dict 17 | mutation_classes: dict 18 | mutations_enum_class: dict 19 | simple_type_classes: dict 20 | type_classes: dict 21 | enum_classes: dict 22 | scalar_defs: dict 23 | type_refs: dict 24 | 25 | def __init__(self): 26 | self.scalar_defs = {} 27 | self.enum_classes = {} 28 | self.simple_type_classes = {} 29 | self.type_classes = {} 30 | self.query_classes = {} 31 | self.queries_enum_class = {} 32 | self.mutation_classes = {} 33 | self.mutations_enum_class = {} 34 | self.type_refs = {} 35 | -------------------------------------------------------------------------------- /tests/output/gdbc_nodesc/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class DistanceUnit(Enum): 4 | DEFAULT = None 5 | KM = 'KM' ##Kilometers 6 | MI = 'MI' ##Miles 7 | 8 | class IncludeDeletedFilterType(Enum): 9 | DEFAULT = None 10 | ALL = 'ALL' ##All data, regardless of if/when marked deleted 11 | SINCE_YESTERDAY = 'SINCE_YESTERDAY' ##Only data not marked deleted before yesterday 12 | SINCE_LAST_WEEK = 'SINCE_LAST_WEEK' ##Only data not marked deleted before last week 13 | NONE = 'NONE' ##Only data not marked deleted 14 | 15 | class Language(Enum): 16 | DEFAULT = None 17 | DE = 'DE' ##German 18 | EN = 'EN' ##English 19 | ES = 'ES' ##Spanish 20 | FR = 'FR' ##French 21 | IT = 'IT' ##Italian 22 | PT = 'PT' ##Portuguese 23 | PT_BR = 'PT_BR' ##Portuguese (Brazil) 24 | RU = 'RU' ##Russian 25 | 26 | class PopulatedPlaceType(Enum): 27 | DEFAULT = None 28 | ADM2 = 'ADM2' ##A level-2 administrative division (for example, a county) 29 | CITY = 'CITY' ##A city, town, or village 30 | ISLAND = 'ISLAND' ##An island 31 | -------------------------------------------------------------------------------- /tests/output/github_nodesc/scalars.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * 2 | 3 | Base64String = str 4 | 5 | BigInt = str 6 | 7 | Boolean = bool 8 | 9 | Date = str 10 | 11 | DateTime = str 12 | 13 | Float = float 14 | 15 | GitObjectID = str 16 | 17 | GitSSHRemote = str 18 | 19 | GitTimestamp = str 20 | 21 | HTML = str 22 | 23 | ID = ID 24 | 25 | Int = int 26 | 27 | PreciseDateTime = str 28 | 29 | String = str 30 | 31 | URI = str 32 | 33 | X509Certificate = str 34 | 35 | isRequired = bool 36 | 37 | trackedIssuesCount = int 38 | 39 | viewerMergeBodyText = str 40 | 41 | viewerMergeHeadlineText = str 42 | 43 | totalIssueContributions = int 44 | 45 | totalPullRequestContributions = int 46 | 47 | totalRepositoriesWithContributedIssues = int 48 | 49 | totalRepositoriesWithContributedPullRequests = int 50 | 51 | totalRepositoryContributions = int 52 | 53 | text = str 54 | 55 | isSponsoredBy = bool 56 | 57 | totalSponsorshipAmountAsSponsorInCents = int 58 | 59 | anyPinnableItems = bool 60 | 61 | canReceiveOrganizationEmailsWhenNotificationsRestricted = bool 62 | 63 | organizationVerifiedDomainEmails = str 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 dapalex 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 | -------------------------------------------------------------------------------- /codegen/src/enums.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | 4 | class TypeKind(Enum): 5 | SCALAR = "SCALAR" #'Indicates this type is a scalar.' 6 | OBJECT = "OBJECT" #'Indicates this type is an object. `fields` and `interfaces` are valid fields.', 7 | ENUM = "ENUM" #'Indicates this type is an enum. `enumValues` is a valid field.', 8 | LIST = "LIST" #'Indicates this type is a list. `ofType` is a valid field.', 9 | INTERFACE = "INTERFACE" #'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.', 10 | UNION = "UNION" #'Indicates this type is a union. `possibleTypes` is a valid field.', 11 | INPUT_OBJECT = "INPUT_OBJECT" #'Indicates this type is an input object. `inputFields` is a valid field.', 12 | NON_NULL = "NON_NULL" #'Indicates this type is a non-null. `ofType` is a valid field.', 13 | 14 | class TemplateType(Enum): 15 | SCALAR_TEMPLATE = 'scalar_template.py' 16 | ENUM_TEMPLATE = 'enum_template.py' 17 | FREE_TYPE_TEMPLATE = 'simple_type_template.py' 18 | QUERY_TEMPLATE = 'query_template.py' 19 | MUTATION_TEMPLATE = 'mutation_template.py' 20 | TYPE_TEMPLATE = 'type_template.py' 21 | TYPE_REFS_TEMPLATE = 'type_refs_template.py' 22 | -------------------------------------------------------------------------------- /codegen/generator.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging as logger 3 | from .src.extractor import Extractor 4 | from .src.printer import Printer 5 | from .src.sp_builder import SchemaBuilder 6 | from .src.sp_schema import GQLSchema 7 | 8 | def build_schema(schemaString: GQLSchema): 9 | myBuilder = SchemaBuilder() 10 | return myBuilder.build(json.loads(schemaString), GQLSchema()) 11 | 12 | class CodeGenerator(): 13 | 14 | def generate_code(schemaObj, folder, log_progress: bool = False, add_desc: bool = True): 15 | try: 16 | if log_progress: logger.info('Initializing Extractor...') 17 | extractor = Extractor(schemaObj, log_progress, add_desc) 18 | if log_progress: logger.info('Extracting schema...') 19 | extractedData = extractor.extract_schema_code() 20 | if log_progress: logger.info('Generating python code...') 21 | printer = Printer(extractedData) 22 | if log_progress: logger.info('Saving python files in ' + folder + '...') 23 | printer.save_files(folder) 24 | 25 | #CLEANING 26 | del extractor 27 | del printer 28 | except Exception as ex: 29 | raise ex 30 | 31 | -------------------------------------------------------------------------------- /tests/output/github/type_refs.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | 5 | 6 | ProjectV2ItemConnection = TypeVar('ProjectV2ItemConnection', bound=GQLObject) 7 | 8 | PullRequestConnection = TypeVar('PullRequestConnection', bound=GQLObject) 9 | 10 | Repository = TypeVar('Repository', bound=GQLObject) 11 | 12 | Project = TypeVar('Project', bound=GQLObject) 13 | 14 | Topic = TypeVar('Topic', bound=GQLObject) 15 | 16 | CommitCommentConnection = TypeVar('CommitCommentConnection', bound=GQLObject) 17 | 18 | Issue = TypeVar('Issue', bound=GQLObject) 19 | 20 | ProjectV2 = TypeVar('ProjectV2', bound=GQLObject) 21 | 22 | PullRequest = TypeVar('PullRequest', bound=GQLObject) 23 | 24 | Ref = TypeVar('Ref', bound=GQLObject) 25 | 26 | SponsorsActivityConnection = TypeVar('SponsorsActivityConnection', bound=GQLObject) 27 | 28 | Sponsorship = TypeVar('Sponsorship', bound=GQLObject) 29 | 30 | SponsorshipConnection = TypeVar('SponsorshipConnection', bound=GQLObject) 31 | 32 | Organization = TypeVar('Organization', bound=GQLObject) 33 | 34 | IpAllowListEntryConnection = TypeVar('IpAllowListEntryConnection', bound=GQLObject) 35 | 36 | WorkflowRunConnection = TypeVar('WorkflowRunConnection', bound=GQLObject) 37 | -------------------------------------------------------------------------------- /tests/output/github_nodesc/type_refs.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, List 2 | from pygqlmap.components import GQLObject 3 | from pygqlmap.gql_types import ID 4 | 5 | 6 | ProjectV2ItemConnection = TypeVar('ProjectV2ItemConnection', bound=GQLObject) 7 | 8 | PullRequestConnection = TypeVar('PullRequestConnection', bound=GQLObject) 9 | 10 | Repository = TypeVar('Repository', bound=GQLObject) 11 | 12 | Project = TypeVar('Project', bound=GQLObject) 13 | 14 | Topic = TypeVar('Topic', bound=GQLObject) 15 | 16 | CommitCommentConnection = TypeVar('CommitCommentConnection', bound=GQLObject) 17 | 18 | Issue = TypeVar('Issue', bound=GQLObject) 19 | 20 | ProjectV2 = TypeVar('ProjectV2', bound=GQLObject) 21 | 22 | PullRequest = TypeVar('PullRequest', bound=GQLObject) 23 | 24 | Ref = TypeVar('Ref', bound=GQLObject) 25 | 26 | SponsorsActivityConnection = TypeVar('SponsorsActivityConnection', bound=GQLObject) 27 | 28 | Sponsorship = TypeVar('Sponsorship', bound=GQLObject) 29 | 30 | SponsorshipConnection = TypeVar('SponsorshipConnection', bound=GQLObject) 31 | 32 | Organization = TypeVar('Organization', bound=GQLObject) 33 | 34 | IpAllowListEntryConnection = TypeVar('IpAllowListEntryConnection', bound=GQLObject) 35 | 36 | WorkflowRunConnection = TypeVar('WorkflowRunConnection', bound=GQLObject) 37 | -------------------------------------------------------------------------------- /tests/test_codegen.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import pathlib 4 | import unittest 5 | 6 | 7 | sys.path.append(str(pathlib.Path(os.path.dirname(sys.path[0])).absolute())) 8 | sys.path.append(str(pathlib.Path(os.path.dirname(sys.path[0]), 'test').absolute())) 9 | 10 | from .gdbc_unittest import (run_fetch_gdbc_schema, run_fetch_gdbc_schema_no_desc) 11 | from .gh_unittest import (run_fetch_gh_schema, run_fetch_gh_schema_no_desc) 12 | from .ra_unittest import (run_fetch_ra_schema, run_fetch_ra_schema_no_desc) 13 | from .re_unittest import (run_fetch_re_schema, run_fetch_re_schema_no_desc) 14 | 15 | class TestCodegen(unittest.TestCase): 16 | 17 | def test_gdbc_schema(self): 18 | return run_fetch_gdbc_schema() 19 | 20 | def test_gdbc_schema_no_desc(self): 21 | return run_fetch_gdbc_schema_no_desc() 22 | 23 | def test_gh_schema(self): 24 | return run_fetch_gh_schema() 25 | 26 | def test_gh_schema_no_desc(self): 27 | return run_fetch_gh_schema_no_desc() 28 | 29 | def test_ra_schema(self): 30 | return run_fetch_ra_schema() 31 | 32 | def test_ra_schema_nodesc(self): 33 | return run_fetch_ra_schema_no_desc() 34 | 35 | def test_re_schema(self): 36 | return run_fetch_re_schema() 37 | 38 | def test_re_schema_nodesc(self): 39 | return run_fetch_re_schema_no_desc() 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/test-codegen.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Code Generation 5 | 6 | on: 7 | push: 8 | branches: [ "develop" ] 9 | pull_request: 10 | branches: [ "develop" ] 11 | 12 | jobs: 13 | build: 14 | environment: develop 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install requests 31 | python -m pip install pytest 32 | python setup.py install 33 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 34 | - name: Test code generation with pytest 35 | run: | 36 | pytest ./tests/test_codegen.py 37 | env: 38 | GEODBCITIES_APIKEY: ${{ secrets.GEODBCITIES_APIKEY }} 39 | GH_APIKEY: ${{ secrets.GH_APIKEY }} 40 | RAPIDAPI_APIKEY: ${{ secrets.RAPIDAPI_APIKEY }} 41 | -------------------------------------------------------------------------------- /tests/output/rapidapi/scalars.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * 2 | 3 | String = str ##The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. 4 | 5 | Boolean = bool ##The `Boolean` scalar type represents `true` or `false`. 6 | 7 | ID = ID ##The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. 8 | 9 | Int = int ##The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. 10 | 11 | DateTime = str ##Our custom date scalar 12 | 13 | Float = float ##The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). 14 | 15 | Any = str ##Dynamic type, returns a value as is. 16 | 17 | Upload = str ##The `Upload` scalar type represents a file upload. 18 | 19 | JSONObject = str ##The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). 20 | 21 | name = str 22 | -------------------------------------------------------------------------------- /.github/workflows/test-map.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Pyhon-GraphQL Mapping 5 | 6 | on: 7 | push: 8 | branches: [ "develop" ] 9 | pull_request: 10 | branches: [ "develop" ] 11 | 12 | jobs: 13 | build: 14 | environment: develop 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install requests 31 | python -m pip install pytest 32 | python setup.py install 33 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 34 | - uses: actions/checkout@v3 35 | - name: Test mapper with pytest 36 | run: | 37 | pytest ./tests/test_mapping.py 38 | env: 39 | GEODBCITIES_APIKEY: ${{ secrets.GEODBCITIES_APIKEY }} 40 | GH_APIKEY: ${{ secrets.GH_APIKEY }} 41 | RAPIDAPI_APIKEY: ${{ secrets.RAPIDAPI_APIKEY }} 42 | -------------------------------------------------------------------------------- /tests/gdbc_unittest.py: -------------------------------------------------------------------------------- 1 | from codegen.network import fetch_schema_obj 2 | from codegen.generator import CodeGenerator 3 | from codegen.query_presets import QUERY_SCHEMA_AND_TYPES 4 | from .consts import GDBC_HEADERS, GDBC_URL 5 | import logging as logger 6 | 7 | def run_fetch_gdbc_schema(): 8 | logger.debug('\n\nRunning run_fetch_gdbc_schema...') 9 | 10 | try: 11 | gqlSchema = fetch_schema_obj(GDBC_URL, GDBC_HEADERS, QUERY_SCHEMA_AND_TYPES) 12 | 13 | if gqlSchema: 14 | logger.debug('Generating python types from GraphQL data...') 15 | CodeGenerator.generate_code(gqlSchema, folder='tests\\output\\gdbc\\', log_progress=True) 16 | logger.debug('Python types generated') 17 | 18 | except Exception as ex: 19 | raise ex 20 | 21 | logger.debug("End of run_fetch_gdbc_schema") 22 | 23 | def run_fetch_gdbc_schema_no_desc(): 24 | logger.debug('\n\nRunning run_fetch_gdbc_schema_no_desc...') 25 | 26 | try: 27 | gqlSchema = fetch_schema_obj(GDBC_URL, GDBC_HEADERS, QUERY_SCHEMA_AND_TYPES) 28 | 29 | if gqlSchema: 30 | logger.debug('Generating python types from GraphQL data...') 31 | CodeGenerator.generate_code(gqlSchema, folder='tests\\output\\gdbc_nodesc\\', add_desc=False, log_progress=True) 32 | logger.debug('Python types generated') 33 | except Exception as ex: 34 | raise ex 35 | 36 | logger.debug("End of run_fetch_gdbc_schema_no_desc") 37 | -------------------------------------------------------------------------------- /tests/output/gdbc/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class DistanceUnit(Enum): 4 | """ 5 | DistanceUnit - The unit of distance to use when considering a distance arg (for example, in location-related criteria) 6 | 7 | """ 8 | DEFAULT = None 9 | KM = 'KM' ##Kilometers 10 | MI = 'MI' ##Miles 11 | 12 | class IncludeDeletedFilterType(Enum): 13 | """ 14 | IncludeDeletedFilterType - What level of stale data is ok to pull 15 | 16 | """ 17 | DEFAULT = None 18 | ALL = 'ALL' ##All data, regardless of if/when marked deleted 19 | SINCE_YESTERDAY = 'SINCE_YESTERDAY' ##Only data not marked deleted before yesterday 20 | SINCE_LAST_WEEK = 'SINCE_LAST_WEEK' ##Only data not marked deleted before last week 21 | NONE = 'NONE' ##Only data not marked deleted 22 | 23 | class Language(Enum): 24 | """ 25 | Language - The languages currently supported 26 | 27 | """ 28 | DEFAULT = None 29 | DE = 'DE' ##German 30 | EN = 'EN' ##English 31 | ES = 'ES' ##Spanish 32 | FR = 'FR' ##French 33 | IT = 'IT' ##Italian 34 | PT = 'PT' ##Portuguese 35 | PT_BR = 'PT_BR' ##Portuguese (Brazil) 36 | RU = 'RU' ##Russian 37 | 38 | class PopulatedPlaceType(Enum): 39 | """ 40 | PopulatedPlaceType - The populated-place types currently supported 41 | 42 | """ 43 | DEFAULT = None 44 | ADM2 = 'ADM2' ##A level-2 administrative division (for example, a county) 45 | CITY = 'CITY' ##A city, town, or village 46 | ISLAND = 'ISLAND' ##An island 47 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "develop" ] 9 | pull_request: 10 | branches: [ "develop" ] 11 | 12 | jobs: 13 | build: 14 | environment: develop 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Check setup 39 | run: | 40 | python setup.py install 41 | -------------------------------------------------------------------------------- /tests/output/gdbc/gql_simple_types.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, List 2 | from pygqlmap.components import GQLArgsSet, GQLObject 3 | from pygqlmap.gql_types import * 4 | from pygqlmap.src.arg_builtin import * 5 | from .enums import * 6 | from .scalars import * 7 | from .type_refs import * 8 | 9 | class FRXHX_distance_Field(ArguedFloat): 10 | """ 11 | FRXHX_distance_Field - The distance result from some location-based query 12 | This field has two forms: 13 | - As a property (e.g., place.distance), returns the distance as part of a query returning places sorted by distance. 14 | - As a function (e.g., place.distance(toPlaceId), returns the distance to the specified place. 15 | 16 | """ 17 | class floatArgs(GQLArgsSet, GQLObject): 18 | """ 19 | toPlaceId - The distance to this place 20 | 21 | distanceUnit - The unit of distance to use 22 | 23 | """ 24 | toPlaceId: ID 25 | distanceUnit: DistanceUnit 26 | 27 | _args: floatArgs 28 | 29 | 30 | 31 | class TimeZone(GQLObject): 32 | """ 33 | TimeZone - A time-zone 34 | 35 | """ 36 | id: ID 37 | name: str 38 | rawUtcOffsetHours: int 39 | dateTime: str 40 | time: str 41 | 42 | class Locale(GQLObject): 43 | """ 44 | Locale - A regional locale representing some country/language combination 45 | 46 | """ 47 | code: ID 48 | name: str 49 | 50 | class Currency(GQLObject): 51 | """ 52 | Currency - A country currency 53 | 54 | """ 55 | countryCodes: list[ID] 56 | code: ID 57 | symbol: str 58 | -------------------------------------------------------------------------------- /codegen/network.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.network import GQLResponse, send_http_request 2 | from .src.sp_builder import SchemaBuilder, SchemaTypeBuilder 3 | from .src.sp_schema import GQLSchema, SCType 4 | 5 | def fetch_schema_response(url, httpHeaders, query): 6 | try: 7 | payload = { "query": query } 8 | response = send_http_request(url, payload, httpHeaders) 9 | except Exception as ex: 10 | raise ex 11 | 12 | return GQLSchemaResponse(response) 13 | 14 | def fetch_schema_obj(url, headers, query): 15 | gql_response = fetch_schema_response(url, headers, query) 16 | gql_response.map_gqldata_to_obj() 17 | gql_response.print_msg_out() 18 | return gql_response.result_obj 19 | 20 | class GQLSchemaResponse(GQLResponse): 21 | 22 | def map_gqldata_to_obj(self, mapped_py_obj = None, build_sctype = None): 23 | """_summary_ 24 | 25 | Args: 26 | includeDeprecated (bool, optional): Decides if deprecated values are to be included. 27 | Defaults to True. 28 | 29 | Returns: 30 | _type_: _description_ 31 | """ 32 | 33 | if self.data: 34 | for root_obj in self.data.keys(): 35 | if root_obj == '__schema': #check self.data type 36 | builder = SchemaBuilder() 37 | self.result_obj = builder.build(self.data, GQLSchema()) 38 | elif root_obj == '__type': #check self.data type 39 | builder = SchemaTypeBuilder() 40 | self.result_obj = builder.build(self.data, SCType()) 41 | else: 42 | self.result_obj = None -------------------------------------------------------------------------------- /codegen/src/sp_schema.py: -------------------------------------------------------------------------------- 1 | from .enums import TypeKind 2 | from .base_class import SchemaTypeManager 3 | 4 | class SCEnumValue(): 5 | name: str 6 | description: str 7 | isDeprecated: bool 8 | 9 | class SCOfType(SchemaTypeManager): 10 | kind: str 11 | name: str 12 | ofType: any #SCOfType 13 | 14 | class SCArgType(SchemaTypeManager): 15 | kind: TypeKind 16 | name: str 17 | ofType: SCOfType 18 | 19 | class SCArg(SchemaTypeManager): 20 | name: str 21 | description: str 22 | type: SCArgType 23 | 24 | class SCFieldType(SchemaTypeManager): 25 | kind: str 26 | ofType: SCOfType 27 | 28 | class SCField(SchemaTypeManager): 29 | name: str 30 | description: str 31 | args: list[SCArg] 32 | type: SCFieldType 33 | isDeprecated: bool 34 | 35 | class SCInterface(SchemaTypeManager): 36 | kind: TypeKind 37 | name: str 38 | ofType: SCOfType 39 | 40 | class SCInputField(SchemaTypeManager): 41 | name: str 42 | description: str 43 | type: SCInterface 44 | defaultValue: str 45 | 46 | class SCType(SchemaTypeManager): 47 | kind: str 48 | name: str 49 | description: str 50 | ofType: SCOfType 51 | fields: list[SCField] 52 | interfaces: list[SCInterface] 53 | enumValues: list[SCEnumValue] 54 | possibleTypes: list['SCType'] 55 | inputFields: list[SCInputField] 56 | 57 | class SCOperationType(): 58 | name: str 59 | 60 | class SCDirective(): 61 | name: str 62 | description: str 63 | locations: list[str] 64 | args: list[SCArg] 65 | 66 | class GQLSchema(): 67 | types: dict[str, SCType] 68 | directives: list[SCDirective] 69 | queryType: SCOperationType = SCOperationType() 70 | mutationType: SCOperationType = SCOperationType() 71 | subscriptionType: SCOperationType = SCOperationType() 72 | 73 | 74 | -------------------------------------------------------------------------------- /pygqlmap/src/components.py: -------------------------------------------------------------------------------- 1 | from .utils import get_dot_notation_info, is_none_or_builtin_primitive 2 | import logging as logger 3 | 4 | class FSTree(): 5 | def __init__(self, obj, fieldName: str = None): 6 | self.name = fieldName if fieldName else obj.__class__.__name__ 7 | if hasattr(obj, 'fieldsshow'): 8 | self._fieldsshow = obj._fieldsshow 9 | for field in obj.fieldsshow.keys(): 10 | objField = getattr(obj, field) 11 | if is_none_or_builtin_primitive(objField): continue 12 | if not hasattr(self, 'children'): self.children = [] 13 | self.children.append(FSTree(objField, field)) 14 | 15 | def set_fieldshow(self, fieldName: str, show: bool): 16 | info = get_dot_notation_info(fieldName) 17 | path = info[0] 18 | field = info[1] 19 | 20 | if len(path) == 0: 21 | logger.info('trying to hide the entire object...') 22 | return 23 | 24 | attrContainer = self.findBranchContainer(path) 25 | 26 | if field == '*': 27 | for curr_field_name in attrContainer._fieldsshow.keys(): 28 | attrContainer._fieldsshow[curr_field_name] = show 29 | elif field in attrContainer._fieldsshow.keys(): 30 | attrContainer._fieldsshow[field] = show 31 | else: 32 | raise Exception('field ' + field + ' not found!') 33 | 34 | def findBranchContainer(self, path: str): 35 | attrContainer = self 36 | 37 | while len(path): 38 | field = path.pop(0) 39 | if attrContainer.name == field: ##container object 40 | continue 41 | for child in attrContainer.children: 42 | if child.name == field: 43 | attrContainer = child 44 | return attrContainer 45 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from io import TextIOWrapper 3 | import logging as logger 4 | import os 5 | import pathlib 6 | import sys 7 | 8 | 9 | class OutputType(Enum): 10 | COMMANDS = 1, 11 | SCHEMAS = 2, 12 | OTHER = 3 13 | 14 | def cleanFolder(folder): 15 | for top, dirs, files in os.walk(folder): 16 | for file in files: 17 | os.remove(os.path.join(top,file)) 18 | for dir in dirs: 19 | directory = os.path.join(folder, dir) 20 | cleanFolder(directory) 21 | os.rmdir(directory) 22 | 23 | def cleanOutput(outType: OutputType): 24 | if outType == OutputType.COMMANDS: 25 | dirToClean = pathlib.Path(os.path.join(os.curdir, 'cmd_output')) 26 | if outType == OutputType.SCHEMAS: 27 | dirToClean = pathlib.Path(os.path.join(os.path.join(os.curdir, 'test'), 'output')) 28 | 29 | cleanFolder(dirToClean) 30 | 31 | # def waitForInput(isToWait: str): 32 | # if isToWait and isToWait.lower() == 'y': input('Go on..') 33 | 34 | # def ManageException(message: str): 35 | # logger.critical(message) 36 | 37 | 38 | def redirectOutputToFile(fileName, append: bool = True): 39 | try: 40 | wrapper = open(fileName, 'a' if append else 'w') 41 | sys.stdout = wrapper 42 | return wrapper 43 | except Exception as ex: 44 | logger.error('Error during output redirect - ' + ex.args[0]) 45 | 46 | def restoreOutput(ioWrapper: TextIOWrapper): 47 | try: 48 | ioWrapper.close() 49 | sys.stdout = sys.stdout 50 | except Exception as ex: 51 | logger.error('Error during output restore - ' + ex.args[0]) 52 | 53 | def stringifyresult(py_result): 54 | result: str 55 | import inspect 56 | 57 | if list in inspect.getmro(type(py_result)): 58 | result = str(py_result.__class__) + ' [' 59 | for el in py_result: 60 | result += ' ' + str(el) 61 | 62 | result + ' ]' 63 | return result 64 | else: 65 | return str(py_result) -------------------------------------------------------------------------------- /tests/output/re/scalars.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * 2 | 3 | String = str ##The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. 4 | 5 | ID = ID ##The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. 6 | 7 | Boolean = bool ##The `Boolean` scalar type represents `true` or `false`. 8 | 9 | DateTime = str ##The `DateTime` scalar type represents a DateTime - value as specified by - [iso8601](https://en.wikipedia.org/wiki/ISO_8601). 10 | 11 | Float = float ##The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). 12 | 13 | Int = int ##The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. 14 | 15 | Date = str ##The `Date` scalar type represents a Date - value as specified by - [iso8601](https://en.wikipedia.org/wiki/ISO_8601). 16 | 17 | BigInt = str ##The `BigInt` scalar type represents non-fractional whole numeric values. - `BigInt` is not constrained to 32-bit like the `Int` type and thus is a less - compatible type. 18 | 19 | Decimal = str ##The `Decimal` scalar type represents a python Decimal. 20 | 21 | JSONString = str ##Allows use of a JSON String for input / output from the GraphQL schema. - - Use of this type is *not recommended* as you lose the benefits of having a defined, static - schema (one of the key benefits of GraphQL). 22 | 23 | Upload = str ##Create scalar that ignores normal serialization/deserialization, since - that will be handled by the multipart request spec 24 | 25 | GenericScalar = str ##The `GenericScalar` scalar type represents a generic - GraphQL scalar value that could be: - String, Boolean, Int, Float, List or Object. 26 | -------------------------------------------------------------------------------- /tests/tstquery/simple_obj_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Query to fetch a simple GraphQL type and build the python class instance containing the data from the response 5 | 6 | Note: This is only for descriptive purposes, the GraphQL server involved does not expose such a query, for real-world cases see other tests 7 | 8 | Query to reproduce: 9 | 10 | query MyQuery { 11 | rateLimit { 12 | cost -> Int 13 | limit -> Int 14 | nodeCount -> Int 15 | remaining -> Int 16 | resetAt -> String 17 | used -> Int 18 | } 19 | } 20 | 21 | STEP 1: Instantiate the python class representing the GraphQL query 22 | STEP 2: Query the GraphQL server 23 | STEP 3: Pass the response received to the GQLResponse constructor 24 | STEP 4: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 25 | 26 | RESULT: The object currency within gqlResponse.result_obj will contain the data from the GraphQL server 27 | """ 28 | 29 | import requests 30 | from ..consts import GITHUB_HEADERS, GITHUB_URL 31 | from ..output.github.queries import rateLimit 32 | import logging as logger 33 | from ..utils import stringifyresult 34 | 35 | def run_simple_obj(): 36 | logger.debug('\n\nRunning test_simple_obj...') 37 | ##STEP 2 38 | query = rateLimit() 39 | query.name = 'mySimpleQuery' 40 | ## 41 | 42 | try: 43 | logger.debug('Query GQL syntax: ' + query.export_gql_source) 44 | 45 | ##STEP 3 46 | response = requests.request('POST', url=GITHUB_URL, json={ "query": query.export_gql_source }, headers=GITHUB_HEADERS) 47 | ## 48 | 49 | ##STEP 4 50 | from pygqlmap.network import GQLResponse 51 | 52 | gqlResponse = GQLResponse(response) 53 | ## 54 | 55 | gqlResponse.print_msg_out() 56 | 57 | ##STEP 5 58 | gqlResponse.map_gqldata_to_obj(query.type) 59 | ## 60 | 61 | ##RESULT 62 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 63 | ## 64 | except Exception as ex: 65 | raise ex 66 | 67 | logger.debug("End of test_simple_obj") 68 | -------------------------------------------------------------------------------- /RELEASE_NOTES.MD: -------------------------------------------------------------------------------- 1 | # py-graphql-mapper Release Update 2 | 3 | ## Release notes version: 1.1.2 4 | 5 | Release date: Aug. 11, 2023 6 | 7 | Changes 8 | 9 | * Extended visibility of fields feature allowing usage of symbol '*' in order to set same visibility for all fields in a type 10 | 11 | Bug fixes 12 | 13 | * Operations not exported when payload is scalar type 14 | * List of custom scalar types unusable 15 | 16 | ## Release notes version: 1.1.1 17 | 18 | Release date: Jul. 2, 2023 19 | 20 | Changes 21 | 22 | * Operation types not anymore based on the conventional "Query/Mutation" string definition but checking *__schema -> QueryType/MutationType -> name* definition 23 | 24 | Bug fixes 25 | 26 | * Queries having payload as list return only 1 element of the list: the issue has been solved, also the list type will not show anymore the element type fields 27 | 28 | ## Release notes version: 1.1.0 29 | 30 | Release date: Feb. 10, 2023 31 | 32 | Features 33 | 34 | * Assignment of arguments using kwargs: other than using _arg object fields to set the arguments, now it is also possible to assign arguments using kwargs in the constructor of the parent object. 35 | 36 | Bug fixes 37 | 38 | * Check for NonNullability types: nonnull types will be recognized when using arguments and variables (setting ! in GraphQL syntax). 39 | 40 | Breaking changes: 41 | 42 | Implemented Pythonic naming convention. 43 | The following signatures have changed: 44 | 45 | | Old | New | 46 | |:---------|:-----------| 47 | | fetchSchemaObject | fetch_schema_obj | 48 | | querySchemaAndTypes | QUERY_SCHEMA_AND_TYPES | 49 | | gqlTypes | gql_types | 50 | | gqlArgBuiltin | arg_builtin | 51 | | gqlOperations | gql_operations | 52 | | exportGqlSource | export_gql_source | 53 | | exportGQLVariables | export_gqlvariables | 54 | | printMessageOutput | print_msg_out | 55 | | mapGQLDataToObj | map_gqldata_to_obj | 56 | | setShow | set_show | 57 | | resultObject | result_obj | 58 | | _argsType | _args_type | 59 | | LiteralValues | LITERAL_VALUES | 60 | | Variables | VARIABLES | 61 | 62 | 63 | ## Release notes version: 1.0.0 64 | 65 | Release note date: Jan. 8, 2023 66 | 67 | Overview 68 | 69 | A python library to call a GraphQL API without using hardcoded strings. 70 | Stable version allowing mapping of queries and mutations 71 | 72 | -------------------------------------------------------------------------------- /tests/tstquery/connobj_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Query to fetch a GraphQL connection type and build the python class instance containing the data from the response 5 | 6 | Query to reproduce: 7 | 8 | query MyQuery { 9 | currencies { 10 | totalCount 11 | edges { 12 | cursor 13 | node { 14 | countryCodes -> list of String 15 | code -> String 16 | symbol -> String 17 | } 18 | } 19 | pageInfo { 20 | startCursor 21 | endCursor 22 | hasNextPage 23 | hasPreviousPage 24 | } 25 | } 26 | } 27 | 28 | 29 | STEP 1: Instantiate the python class representing the GraphQL query 30 | STEP 3: Query the GraphQL server 31 | STEP 4: Pass the response received to the GQLResponse constructor 32 | STEP 5: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 33 | 34 | RESULT: The object currencies within gqlResponse.result_obj will contain the data from the GraphQL server 35 | """ 36 | 37 | import requests 38 | from ..consts import GDBC_URL, GDBC_HEADERS 39 | from ..output.gdbc.queries import currencies 40 | import logging as logger 41 | from ..utils import stringifyresult 42 | 43 | def run_gdbc_connobj(): 44 | logger.debug('\n\nRunning test_gdbc_connobj...') 45 | ##STEP 2 46 | 47 | query = currencies() 48 | query.name = 'myCurrenciesQuery' 49 | ## 50 | 51 | try: 52 | logger.debug('Query GQL syntax: ' + query.export_gql_source) 53 | 54 | ##STEP 3 55 | response = requests.request('POST', url=GDBC_URL, 56 | json={ "query": query.export_gql_source }, 57 | headers=GDBC_HEADERS) 58 | ## 59 | 60 | ##STEP 4 61 | from pygqlmap.network import GQLResponse 62 | 63 | gqlResponse = GQLResponse(response) 64 | ## 65 | 66 | gqlResponse.print_msg_out() 67 | 68 | ##STEP 5 69 | gqlResponse.map_gqldata_to_obj(query.type) 70 | ## 71 | 72 | ##RESULT 73 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 74 | ## 75 | except Exception as ex: 76 | raise ex 77 | 78 | logger.debug("End of test_gdbc_connobj") 79 | -------------------------------------------------------------------------------- /tests/tstquery/simple_obj_args_literal_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Query to fetch a simple GraphQL type using Args as Literal Values 5 | and build the python class instance containing the data from the response. 6 | 7 | Note: This is only for descriptive purposes, the GraphQL server involved does not expose such a query, for real-world cases see other tests 8 | 9 | Query to reproduce: 10 | 11 | query MyQuery { 12 | rateLimit { 13 | cost -> Int 14 | limit -> Int 15 | nodeCount -> Int 16 | remaining -> Int 17 | resetAt -> String 18 | used -> Int 19 | } 20 | } 21 | 22 | STEP 1: Instantiate the python class representing the GraphQL query 23 | STEP 2: Set arguments following the object structure with argument type as LITERAL_VALUES 24 | STEP 3: Query the GraphQL server 25 | STEP 4: Pass the response received to the GQLResponse constructor 26 | STEP 5: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 27 | 28 | RESULT: The object currency within gqlResponse.result_obj will contain the data from the GraphQL server 29 | """ 30 | 31 | import requests 32 | from ..consts import GITHUB_HEADERS, GITHUB_URL 33 | from ..output.github.queries import rateLimit 34 | import logging as logger 35 | from ..utils import stringifyresult 36 | 37 | def run_simple_obj_args_literal(): 38 | logger.debug('\n\nRunning run_simple_obj_args_literal...') 39 | ##STEP 2 40 | query = rateLimit() 41 | query.name = 'mySimpleQueryArgs' 42 | ## 43 | 44 | ##STEP 3 45 | query._args.dryRun = True 46 | ## 47 | try: 48 | logger.debug('Query GQL syntax: ' + query.export_gql_source) 49 | 50 | ##STEP 4 51 | response = requests.request('POST', url=GITHUB_URL, json={ "query": query.export_gql_source }, headers=GITHUB_HEADERS) 52 | ## 53 | 54 | ##STEP 5 55 | from pygqlmap.network import GQLResponse 56 | 57 | gqlResponse = GQLResponse(response) 58 | ## 59 | 60 | gqlResponse.print_msg_out() 61 | 62 | ##STEP 6 63 | gqlResponse.map_gqldata_to_obj(query.type) 64 | ## 65 | 66 | ##RESULT 67 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 68 | ## 69 | except Exception as ex: 70 | raise ex 71 | 72 | logger.debug("End of run_simple_obj_args_literal") 73 | -------------------------------------------------------------------------------- /tests/tstquery/simple_obj_viewchange_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Query to fetch a simple GraphQL type 5 | and build the python class instance containing the data from the response. 6 | In addition it shows how to hide fields of a type in a query. 7 | 8 | Query to reproduce: 9 | 10 | query MyQuery { 11 | rateLimit { 12 | cost (*) 13 | limit 14 | nodeCount (*) 15 | remaining 16 | resetAt 17 | used (*) 18 | } 19 | } 20 | 21 | OBJECTIVE: Hiding fields from the original python mapped class (above with (*)) 22 | 23 | STEP 1: Instantiate the python class representing the GraphQL query 24 | STEP 2: Call set_show function of the python class to set the visibility of fields (path to declare with dot notation) 25 | STEP 3: Query the GraphQL server 26 | STEP 4: Pass the response received to the GQLResponse constructor 27 | STEP 5: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 28 | 29 | RESULT: 30 | a) The request toward the GraphQL server will not have the hidden fields 31 | b) The python class instance obtained from the response will not have the hidden fields 32 | """ 33 | 34 | import requests 35 | from ..consts import GITHUB_HEADERS, GITHUB_URL 36 | from ..output.github.queries import rateLimit 37 | import logging as logger 38 | from ..utils import stringifyresult 39 | 40 | def run_simple_obj_viewchange(): 41 | logger.debug('\n\nRunning run_simple_obj_viewchange...') 42 | ##STEP 2 43 | query = rateLimit() 44 | query.name = 'mySimpleQueryVisibility' 45 | ## 46 | 47 | ##STEP 3 48 | query.set_show('rateLimit.cost', False) 49 | query.set_show('rateLimit.nodeCount', False) 50 | query.set_show('rateLimit.used', False) 51 | ## 52 | 53 | try: 54 | ##RESULT a) 55 | logger.debug('Query GQL syntax: ' + query.export_gql_source) 56 | ## 57 | 58 | ##STEP 4 59 | response = requests.request('POST', url=GITHUB_URL, 60 | json={ "query": query.export_gql_source }, 61 | headers=GITHUB_HEADERS) 62 | ## 63 | 64 | ##STEP 5 65 | from pygqlmap.network import GQLResponse 66 | 67 | gqlResponse = GQLResponse(response) 68 | ## 69 | 70 | gqlResponse.print_msg_out() 71 | 72 | ##STEP 6 73 | gqlResponse.map_gqldata_to_obj(query.type) 74 | ## 75 | 76 | ##RESULT b) 77 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 78 | ## 79 | except Exception as ex: 80 | raise ex 81 | 82 | logger.debug("End of run_simple_obj_viewchange") 83 | -------------------------------------------------------------------------------- /tests/tstquery/simple_obj_args_vars_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Query to fetch a simple GraphQL type using Args and Variables 5 | and build the python class instance containing the data from the response. 6 | 7 | Note: This is only for descriptive purposes, the GraphQL server involved does not expose such a query, for real-world cases see other tests 8 | 9 | Query to reproduce: 10 | 11 | query MyQuery { 12 | rateLimit { 13 | cost -> Int 14 | limit -> Int 15 | nodeCount -> Int 16 | remaining -> Int 17 | resetAt -> String 18 | used -> Int 19 | } 20 | } 21 | 22 | STEP 1: Instantiate the python class representing the GraphQL query 23 | STEP 2: Define arguments following object structure with argument type as Variables 24 | STEP 3: Query the GraphQL server 25 | STEP 4: Pass the response received to the GQLResponse constructor 26 | STEP 5: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 27 | 28 | RESULT: The object currency within gqlResponse.result_obj will contain the data from the GraphQL server 29 | """ 30 | 31 | import requests 32 | from ..consts import GITHUB_HEADERS, GITHUB_URL 33 | from ..output.github.queries import rateLimit 34 | import logging as logger 35 | from ..utils import stringifyresult 36 | 37 | def run_simple_args_vars(): 38 | logger.debug('\n\nRunning test_simple_args_vars...') 39 | try: 40 | 41 | ##STEP 2 42 | query = rateLimit() 43 | query.name = 'mySimpleQueryArgsVars' 44 | ## 45 | 46 | ##STEP 2 47 | from pygqlmap.enums import ArgType 48 | 49 | query._args.dryRun = False 50 | query._args_type = ArgType.VARIABLES 51 | ## 52 | 53 | ##RESULT 54 | logger.debug('Query GQL syntax: ' + query.export_gql_source) 55 | logger.debug('Variables: ' + query.export_gqlvariables) 56 | ## 57 | 58 | ##STEP 4 59 | response = requests.request('POST', url=GITHUB_URL, 60 | json={ "query": query.export_gql_source, "variables": query.export_gqlvariables }, 61 | headers=GITHUB_HEADERS) 62 | ## 63 | 64 | ##STEP 5 65 | from pygqlmap.network import GQLResponse 66 | 67 | gqlResponse = GQLResponse(response) 68 | ## 69 | 70 | gqlResponse.print_msg_out() 71 | 72 | ##STEP 6 73 | gqlResponse.map_gqldata_to_obj(query.type) 74 | ## 75 | 76 | ##RESULT 77 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 78 | ## 79 | except Exception as ex: 80 | raise ex 81 | 82 | logger.debug("End of test_simple_args_vars") 83 | -------------------------------------------------------------------------------- /codegen/src/utils.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | import os 3 | import pathlib 4 | from .enums import TypeKind 5 | 6 | ##, TypeKind.ENUM.name could be a big mess 7 | gqlTypeKinds = (TypeKind.INPUT_OBJECT.name, TypeKind.INTERFACE.name, TypeKind.OBJECT.name, TypeKind.UNION.name) 8 | typesByName = [TypeKind.INPUT_OBJECT.name, TypeKind.OBJECT.name, TypeKind.INTERFACE.name, TypeKind.SCALAR.name, TypeKind.ENUM.name, TypeKind.UNION.name] 9 | 10 | def perf_profile_log(): 11 | return open('test/logs/performance analysis.log', 'w') 12 | 13 | def split_types(dictionary: dict[str, any]): 14 | myDeque = deque() 15 | simpleTypes = [] 16 | while dictionary: 17 | try: 18 | currentItem = dictionary.popitem() 19 | currUsedTypes = 0 20 | for field in currentItem[1].get_valid_fields_lst(): 21 | currUsedTypes += len(field.get_used_typenames()) 22 | if currUsedTypes == 0: 23 | simpleTypes.append(currentItem) 24 | else: 25 | minUT = len(myDeque[0][1].get_used_typenames()) if myDeque else 1 26 | if currUsedTypes <= minUT: 27 | myDeque.appendleft(currentItem) 28 | else: 29 | myDeque.append(currentItem) 30 | except Exception as ex: 31 | raise Exception('Error during split of types - ' + ex.args[0]) 32 | 33 | return dict(simpleTypes), dict(myDeque) 34 | 35 | def get_valid_folder(folder): 36 | try: 37 | input_path = pathlib.Path(folder).absolute() 38 | 39 | if not input_path.exists(): 40 | if input_path.parent.absolute().exists(): 41 | os.mkdir(input_path) 42 | except Exception as ex: 43 | raise ex 44 | 45 | return str(input_path) 46 | 47 | def is_deprecated(obj): 48 | return hasattr(obj, 'isDeprecated') and obj.isDeprecated 49 | 50 | def pop_val_clean_dict(value, dictionary: dict[str, list], key = None): 51 | k_to_check = [] 52 | try: 53 | if not key: 54 | for k, v in dictionary.items(): 55 | if value in v: 56 | v.remove(value) 57 | k_to_check.append(k) 58 | else: 59 | dictionary[key].remove(value) 60 | k_to_check.append(key) 61 | 62 | map(lambda key: dictionary.pop(key) if not dictionary[key] else None, k_to_check) 63 | except Exception as ex: 64 | raise ex 65 | 66 | def add_val_update_dict(dictionary: dict, key, value): 67 | try: 68 | if not key in dictionary.keys(): 69 | dictionary.update({ key: value }) 70 | else: 71 | if isinstance(value, list): 72 | dictionary[key].extend(value) 73 | elif isinstance(value, int): 74 | dictionary[key] += value 75 | except Exception as ex: 76 | raise ex -------------------------------------------------------------------------------- /.github/workflows/test-cli.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Command Line Interface 5 | 6 | on: 7 | push: 8 | branches: [ "develop" ] 9 | pull_request: 10 | branches: [ "develop" ] 11 | 12 | jobs: 13 | build: 14 | environment: develop 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install pytest 31 | python setup.py install 32 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 33 | - name: create-json-files 34 | id: create-gdbc-json-downloader 35 | uses: jsdaniell/create-json@v1.2.2 36 | with: 37 | dir: './tests/cli_input/gdbc/' 38 | name: "downloaderArgs.json" 39 | json: ${{ secrets.GDBC_DOWNLOADER_ARGS_JSON }} 40 | - name: create-json-files 41 | id: create-gdbc-json-generator 42 | uses: jsdaniell/create-json@v1.2.2 43 | with: 44 | dir: './tests/cli_input/gdbc/' 45 | name: "generatorArgs.json" 46 | json: ${{ secrets.GDBC_GENERATOR_ARGS_JSON }} 47 | - name: create-json-files 48 | id: create-rapidapi-json-downloader 49 | uses: jsdaniell/create-json@v1.2.2 50 | with: 51 | dir: './tests/cli_input/rapidapi/' 52 | name: "downloaderArgs.json" 53 | json: ${{ secrets.RAPIDAPI_DOWNLOADER_ARGS_JSON }} 54 | - name: create-json-files 55 | id: create-rapidapi-json-generator 56 | uses: jsdaniell/create-json@v1.2.2 57 | with: 58 | dir: './tests/cli_input/rapidapi/' 59 | name: "generatorArgs.json" 60 | json: ${{ secrets.RAPIDAPI_GENERATOR_ARGS_JSON }} 61 | # - name: create-json-files 62 | # id: create-github-json-generator 63 | # uses: jsdaniell/create-json@v1.2.2 64 | # with: 65 | # dir: './tests/cli_input/github/' 66 | # name: "generatorArgs.json" 67 | # json: ${{ secrets.GH_GENERATOR_ARGS_JSON }} 68 | # - name: create-json-files 69 | # id: create-github-json-downloader 70 | # uses: jsdaniell/create-json@v1.2.2 71 | # with: 72 | # dir: './tests/cli_input/github/' 73 | # name: "downloaderArgs.json" 74 | # json: ${{ secrets.GH_DOWNLOADER_ARGS_JSON }} 75 | - name: Test commands 76 | run: | 77 | pytest ./tests/test_cli.py 78 | -------------------------------------------------------------------------------- /tests/tstquery/connobj_args_literal_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Query to fetch a GraphQL connection type using Args as literal values 5 | and build the python class instance containing the data from the response 6 | 7 | PREREQUISITES: 8 | 9 | A mapped connection Query (see _gdbc_connobjTest.py STEP 1): 10 | 11 | GraphQL version 12 | 13 | query MyQuery { 14 | currencies { 15 | totalCount 16 | edges { 17 | cursor 18 | node { 19 | countryCodes 20 | code 21 | symbol 22 | } 23 | } 24 | pageInfo { 25 | startCursor 26 | endCursor 27 | hasNextPage 28 | hasPreviousPage 29 | } 30 | } 31 | } 32 | 33 | OBJECTIVE: Creating a query passing arguments and values 34 | 35 | STEP 1: Instantiate the python class representing the GraphQL query 36 | STEP 2: Define arguments following the object structure with argument type as LITERAL_VALUES 37 | STEP 3: Query the GraphQL server 38 | STEP 4: Pass the response received to the GQLResponse constructor 39 | STEP 5: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 40 | 41 | RESULT: The request toward the GraphQL server will have the query with arguments 'first' and 'after' with values first=3 and after='MTE=' as literal values 42 | """ 43 | 44 | import requests 45 | from ..consts import GDBC_URL, GDBC_HEADERS 46 | from ..output.gdbc.queries import currencies 47 | import logging as logger 48 | from ..utils import stringifyresult 49 | 50 | def run_gdbc_connobj_args_literal(): 51 | logger.debug('\n\nRunning run_gdbc_connobj_args_literal...') 52 | ##STEP 1 53 | query = currencies() 54 | query.name='myCurrenciesQuery' 55 | ## 56 | 57 | ##STEP 2 58 | from pygqlmap.enums import ArgType 59 | 60 | query._args_type = ArgType.LITERAL_VALUES 61 | query._args.last = 7 62 | query._args.before = 'MTE=' 63 | ## 64 | try: 65 | ##RESULT 66 | logger.debug('Query GQL syntax: ' + query.export_gql_source) 67 | ## 68 | 69 | ##STEP 3 70 | response = requests.request('POST', url=GDBC_URL, 71 | json={ "query": query.export_gql_source }, 72 | headers=GDBC_HEADERS) 73 | ## 74 | 75 | ##STEP 4 76 | from pygqlmap.network import GQLResponse 77 | 78 | gqlResponse = GQLResponse(response) 79 | ## 80 | 81 | gqlResponse.print_msg_out() 82 | 83 | ##STEP 5 84 | gqlResponse.map_gqldata_to_obj(query.type) 85 | ## 86 | 87 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 88 | 89 | except Exception as ex: 90 | raise ex 91 | 92 | logger.debug("End of run_gdbc_connobj_args_literal") 93 | -------------------------------------------------------------------------------- /tests/tstquery/connobj_args_vars_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Query to fetch a GraphQL connection type using Args and Variables 5 | and build the python class instance containing the data from the response 6 | 7 | PREREQUISITES: 8 | 9 | A mapped connection Query (see _gdbc_connobjTest.py STEP 1): 10 | 11 | GraphQL version 12 | 13 | query MyQuery { 14 | currencies { 15 | totalCount 16 | edges { 17 | cursor 18 | node { 19 | countryCodes 20 | code 21 | symbol 22 | } 23 | } 24 | pageInfo { 25 | startCursor 26 | endCursor 27 | hasNextPage 28 | hasPreviousPage 29 | } 30 | } 31 | } 32 | 33 | OBJECTIVE: Creating a query passing arguments and values 34 | 35 | STEP 1: Instantiate the python class representing the GraphQL query 36 | STEP 2: Instantiate arguments following the object structure with argument type as Variables 37 | STEP 3: Query the GraphQL server 38 | STEP 4: Pass the response received to the GQLResponse constructor 39 | STEP 5: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 40 | 41 | RESULT: The request toward the GraphQL server will have the query with arguments 'first' and 'after' and the variables with values first=3 and after='MTE=' 42 | """ 43 | 44 | import requests 45 | from ..consts import GDBC_URL, GDBC_HEADERS 46 | from ..output.gdbc.queries import currencies 47 | import logging as logger 48 | from ..utils import stringifyresult 49 | 50 | def run_gdbc_connobj_args_vars(): 51 | logger.debug('\n\nRunning run_gdbc_connobj_args_vars...') 52 | ##STEP 1 53 | query = currencies() 54 | query.name = 'myCurrenciesQuery' 55 | ## 56 | 57 | ##STEP 2 58 | from pygqlmap.enums import ArgType 59 | 60 | query._args.first = 3 61 | query._args.after = 'MTE=' 62 | query._args_type = ArgType.VARIABLES 63 | ## 64 | try: 65 | ##RESULT 66 | logger.debug('Query GQL syntax: ' + query.export_gql_source) 67 | logger.debug('Variables: ' + query.export_gqlvariables) 68 | ## 69 | 70 | ##STEP 3 71 | response = requests.request('POST', url=GDBC_URL, 72 | json={ "query": query.export_gql_source, "variables": query.export_gqlvariables }, 73 | headers=GDBC_HEADERS) 74 | ## 75 | 76 | ##STEP 4 77 | from pygqlmap.network import GQLResponse 78 | 79 | gqlResponse = GQLResponse(response) 80 | ## 81 | 82 | gqlResponse.print_msg_out() 83 | 84 | ##STEP 5 85 | gqlResponse.map_gqldata_to_obj(query.type) 86 | ## 87 | 88 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 89 | 90 | except Exception as ex: 91 | raise ex 92 | 93 | logger.debug("End of run_gdbc_connobj_args_vars") 94 | -------------------------------------------------------------------------------- /tests/output/github/scalars.py: -------------------------------------------------------------------------------- 1 | from pygqlmap.gql_types import * 2 | 3 | Base64String = str ##A (potentially binary) string encoded using base64. 4 | 5 | BigInt = str ##Represents non-fractional signed whole numeric values. Since the value may exceed the size of a 32-bit integer, it's encoded as a string. 6 | 7 | Boolean = bool ##Represents `true` or `false` values. 8 | 9 | Date = str ##An ISO-8601 encoded date string. 10 | 11 | DateTime = str ##An ISO-8601 encoded UTC date string. 12 | 13 | Float = float ##Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). 14 | 15 | GitObjectID = str ##A Git object ID. 16 | 17 | GitSSHRemote = str ##Git SSH string 18 | 19 | GitTimestamp = str ##An ISO-8601 encoded date string. Unlike the DateTime type, GitTimestamp is not converted in UTC. 20 | 21 | HTML = str ##A string containing HTML code. 22 | 23 | ID = ID ##Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"VXNlci0xMA=="`) or integer (such as `4`) input value will be accepted as an ID. 24 | 25 | Int = int ##Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. 26 | 27 | PreciseDateTime = str ##An ISO-8601 encoded UTC date string with millisecond precision. 28 | 29 | String = str ##Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text. 30 | 31 | URI = str ##An RFC 3986, RFC 3987, and RFC 6570 (level 4) compliant URI string. 32 | 33 | X509Certificate = str ##A valid x509 certificate string 34 | 35 | isRequired = bool ##Whether this is required to pass before merging for a specific pull request. 36 | 37 | trackedIssuesCount = int ##The number of tracked issues for this issue 38 | 39 | viewerMergeBodyText = str ##The merge body text for the viewer and method. 40 | 41 | viewerMergeHeadlineText = str ##The merge headline text for the viewer and method. 42 | 43 | totalIssueContributions = int ##How many issues the user opened. 44 | 45 | totalPullRequestContributions = int ##How many pull requests the user opened. 46 | 47 | totalRepositoriesWithContributedIssues = int ##How many different repositories the user opened issues in. 48 | 49 | totalRepositoriesWithContributedPullRequests = int ##How many different repositories the user opened pull requests in. 50 | 51 | totalRepositoryContributions = int ##How many repositories the user created. 52 | 53 | text = str ##UTF8 text data or null if the file is binary 54 | 55 | isSponsoredBy = bool ##Whether the given account is sponsoring this user/organization. 56 | 57 | totalSponsorshipAmountAsSponsorInCents = int ##The amount in United States cents (e.g., 500 = $5.00 USD) that this entity has spent on GitHub to fund sponsorships. Only returns a value when viewed by the user themselves or by a user who can manage sponsorships for the requested organization. 58 | 59 | anyPinnableItems = bool ##Determine if this repository owner has any items that can be pinned to their profile. 60 | 61 | canReceiveOrganizationEmailsWhenNotificationsRestricted = bool ##Could this user receive email notifications, if the organization had notification restrictions enabled? 62 | 63 | organizationVerifiedDomainEmails = str ##Verified email addresses that match verified domains for a specified organization the user is a member of. 64 | -------------------------------------------------------------------------------- /tests/output/gdbc_nodesc/queries.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from pygqlmap import GQLQuery 3 | from .gql_types import * 4 | from .gql_simple_types import * 5 | from .enums import * 6 | from .scalars import * 7 | from .type_refs import * 8 | 9 | class country(GQLQuery): 10 | class CountryArgs(GQLArgsSet, GQLObject): 11 | id: NonNull_ID 12 | displayOptions: DisplayOptions 13 | 14 | _args: CountryArgs 15 | 16 | 17 | type: Country 18 | 19 | class countries(GQLQuery): 20 | class CountriesConnectionArgs(GQLArgsSet, GQLObject): 21 | currencyCode: str 22 | namePrefix: str 23 | namePrefixDefaultLangResults: bool 24 | sort: str 25 | first: int 26 | after: str 27 | last: int 28 | before: str 29 | displayOptions: DisplayOptions 30 | 31 | _args: CountriesConnectionArgs 32 | 33 | 34 | type: CountriesConnection 35 | 36 | class currencies(GQLQuery): 37 | class CurrenciesConnectionArgs(GQLArgsSet, GQLObject): 38 | countryId: ID 39 | first: int 40 | after: str 41 | last: int 42 | before: str 43 | 44 | _args: CurrenciesConnectionArgs 45 | 46 | 47 | type: CurrenciesConnection 48 | 49 | class distanceBetween(GQLQuery): 50 | class floatArgs(GQLArgsSet, GQLObject): 51 | fromPlaceId: NonNull_ID 52 | toPlaceId: NonNull_ID 53 | distanceUnit: DistanceUnit 54 | 55 | _args: floatArgs 56 | 57 | 58 | type: float 59 | 60 | class locales(GQLQuery): 61 | class LocalesConnectionArgs(GQLArgsSet, GQLObject): 62 | first: int 63 | after: str 64 | last: int 65 | before: str 66 | 67 | _args: LocalesConnectionArgs 68 | 69 | 70 | type: LocalesConnection 71 | 72 | class populatedPlace(GQLQuery): 73 | class PopulatedPlaceArgs(GQLArgsSet, GQLObject): 74 | id: NonNull_ID 75 | displayOptions: DisplayOptions 76 | 77 | _args: PopulatedPlaceArgs 78 | 79 | 80 | type: PopulatedPlace 81 | 82 | class populatedPlaces(GQLQuery): 83 | class PopulatedPlacesConnectionArgs(GQLArgsSet, GQLObject): 84 | location: Location 85 | radius: float 86 | distanceUnit: DistanceUnit 87 | countryIds: list[ID] 88 | excludedCountryIds: list[ID] 89 | namePrefix: str 90 | namePrefixDefaultLangResults: bool 91 | minPopulation: int 92 | maxPopulation: int 93 | timeZoneIds: list[ID] 94 | types: list[str] 95 | sort: str 96 | first: int 97 | after: str 98 | last: int 99 | before: str 100 | displayOptions: DisplayOptions 101 | includeDeleted: IncludeDeletedFilterType 102 | 103 | _args: PopulatedPlacesConnectionArgs 104 | 105 | 106 | type: PopulatedPlacesConnection 107 | 108 | class timeZone(GQLQuery): 109 | class TimeZoneArgs(GQLArgsSet, GQLObject): 110 | id: NonNull_ID 111 | 112 | _args: TimeZoneArgs 113 | 114 | 115 | type: TimeZone 116 | 117 | class timeZones(GQLQuery): 118 | class TimeZonesConnectionArgs(GQLArgsSet, GQLObject): 119 | first: int 120 | after: str 121 | last: int 122 | before: str 123 | 124 | _args: TimeZonesConnectionArgs 125 | 126 | 127 | type: TimeZonesConnection 128 | 129 | 130 | class Queries(Enum): 131 | country = country 132 | countries = countries 133 | currencies = currencies 134 | distanceBetween = distanceBetween 135 | locales = locales 136 | populatedPlace = populatedPlace 137 | populatedPlaces = populatedPlaces 138 | timeZone = timeZone 139 | timeZones = timeZones 140 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Keys 79 | test/consts.py 80 | 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 109 | __pypackages__/ 110 | 111 | # Celery stuff 112 | celerybeat-schedule 113 | celerybeat.pid 114 | 115 | # SageMath parsed files 116 | *.sage.py 117 | 118 | # Environments 119 | .env 120 | .venv 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | 127 | # Spyder project settings 128 | .spyderproject 129 | .spyproject 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # mkdocs documentation 135 | /site 136 | 137 | # mypy 138 | .mypy_cache/ 139 | .dmypy.json 140 | dmypy.json 141 | 142 | # Pyre type checker 143 | .pyre/ 144 | 145 | # pytype static type analyzer 146 | .pytype/ 147 | 148 | # Cython debug symbols 149 | cython_debug/ 150 | 151 | # PyCharm 152 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 153 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 154 | # and can be added to the global gitignore or merged into this file. For a more nuclear 155 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 156 | #.idea/ 157 | .vs/* 158 | .vscode/launch.json 159 | codegen/codegenlaunch.json 160 | .vscode/settings__.json 161 | tests/spotTest.py 162 | tests/cmd_output/* 163 | .vscode/launch.json 164 | .vscode/settings.json 165 | pytest_log.txt 166 | -------------------------------------------------------------------------------- /tests/tstquery/connobj_viewchange_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Query to fetch a GraphQL connection type 5 | and build the python class instance containing the data from the response. 6 | In addition it shows how to hide fields of a type in a query. 7 | 8 | PREREQUISITES: 9 | 10 | A mapped connection Query (see _gdbc_connobjTest.py STEP 1): 11 | 12 | GraphQL version 13 | 14 | query MyQuery { 15 | currencies { 16 | totalCount (*) 17 | edges { 18 | cursor 19 | node { 20 | countryCodes (*) 21 | code 22 | symbol (*) 23 | } 24 | } 25 | pageInfo { 26 | startCursor 27 | endCursor 28 | hasNextPage 29 | hasPreviousPage 30 | } 31 | } 32 | } 33 | 34 | Relating python classes 35 | 36 | class currency(GQLObject): 37 | countryCodes: list 38 | code: str 39 | symbol: str 40 | 41 | def __init__(self): 42 | self.countryCodes = [] 43 | self.code = '' 44 | self.symbol = '' 45 | super().__init__() 46 | 47 | class currencies(GQLConnection): 48 | 49 | def __init__(self): 50 | super().__init__(GQLEdges(currency())) 51 | 52 | OBJECTIVE: Hiding fields from the original python mapped class (above with (*)) 53 | 54 | STEP 1: Instantiate python class representing the GraphQL query 55 | STEP 2: Call set_show function of the object to set the visibility of field (path to declare with dot notation) 56 | STEP 3: Query the GraphQL server 57 | STEP 4: Pass the response received to the GQLResponse constructor 58 | STEP 5: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 59 | 60 | RESULT: 61 | a) The request toward the GraphQL server will not have the hidden fields 62 | b) The python class instance obtained from the response will not have the hidden fields 63 | """ 64 | 65 | import requests 66 | from ..consts import GDBC_URL, GDBC_HEADERS 67 | from ..output.gdbc.queries import currencies 68 | import logging as logger 69 | from ..utils import stringifyresult 70 | 71 | def run_gdbc_connobj_viewchange(): 72 | logger.debug('\n\nRunning run_gdbc_connobj_viewchange...') 73 | ##STEP 1 74 | from pygqlmap.enums import OperationType 75 | from pygqlmap.components import GQLOperation 76 | 77 | query = currencies() 78 | query.name = 'myCurrenciesQueryVisibility' 79 | ## 80 | 81 | ##STEP 2 82 | query.set_show('currencies.edges.node.symbol', False) 83 | query.set_show('currencies.edges.node.countryCodes', False) 84 | query.set_show('currencies.totalCount', False) 85 | ## 86 | 87 | try: 88 | ##RESULT a) 89 | logger.debug('Query GQL syntax: ' + query.export_gql_source) 90 | ## 91 | 92 | ##STEP 3 93 | response = requests.request('POST', url=GDBC_URL, 94 | json={ "query": query.export_gql_source }, 95 | headers=GDBC_HEADERS) 96 | ## 97 | 98 | ##STEP 4 99 | from pygqlmap.network import GQLResponse 100 | 101 | gqlResponse = GQLResponse(response) 102 | ## 103 | 104 | gqlResponse.print_msg_out() 105 | 106 | ##STEP 5 107 | gqlResponse.map_gqldata_to_obj(query.type) 108 | ## 109 | 110 | ##RESULT b) 111 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 112 | ## 113 | except Exception as ex: 114 | raise ex 115 | 116 | logger.debug("End of run_gdbc_connobj_viewchange") 117 | -------------------------------------------------------------------------------- /pygqlmap/src/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from types import GenericAlias 3 | import typing 4 | import logging as logger 5 | import re 6 | import sys 7 | from enum import Enum 8 | from pygqlmap.gql_types import ID 9 | from pygqlmap.helper import handle_recursive_ex 10 | from .consts import NON_NULL_PREFIX, PRIMITIVES 11 | 12 | stdOut = sys.stdout 13 | 14 | def is_empty_field(field): 15 | curr_type = field.__class__ 16 | if type(field) == GenericAlias: 17 | if field.__name__.startswith(NON_NULL_PREFIX): 18 | curr_type = inspect.getmro(field.__origin__)[1] #get parent 19 | if field.__class__.__name__.startswith(NON_NULL_PREFIX): 20 | curr_type = inspect.getmro(type(field))[1] 21 | if curr_type == str or curr_type == ID: 22 | return len(field) == 0 23 | elif curr_type == int or curr_type == float: 24 | return field < 0 25 | elif curr_type == bool: 26 | return False 27 | elif curr_type == dict: 28 | return not not field 29 | elif curr_type == list: 30 | return not field 31 | elif curr_type == type: #should never get in 32 | return field 33 | elif Enum in inspect.getmro(curr_type): 34 | return field.value == None 35 | elif inspect.isclass(curr_type): 36 | out = True 37 | for innerField in field.__dataclass_fields__: 38 | out = out and is_empty_field(getattr(field, innerField)) 39 | return out 40 | else: 41 | logger.error('type not managed!') 42 | 43 | def is_none_or_builtin_primitive(obj): 44 | return type(obj) in PRIMITIVES 45 | 46 | def pop_lst_element_by_ref(lst: list, element): 47 | while lst.index(element) >= 0: 48 | lstEl = lst[lst.index(element)] 49 | if id(lstEl) == id(element): 50 | return lst.pop(lst.index(element)) 51 | 52 | def get_dot_notation_info(dataInput: str) -> tuple: 53 | """for internal use 54 | 55 | Args: 56 | input (str): path through dot notation to a variable 57 | e.g.: .. 58 | 59 | Returns: 60 | tuple: structured version of input in 61 | ([path through objects], ) 62 | """ 63 | path = dataInput.split('.') 64 | variable = path.pop(len(path) - 1) 65 | return (path, variable) 66 | 67 | def execute_regex(dataInput): 68 | ret = re.sub(r': *\'[!-&(-z]*\'', ' ', dataInput) #removes anything after : -> : '' 69 | ret = re.sub(r': *[-A-Za-z0-9]*', ' ', ret) 70 | ret = ret.replace('\'', ' ').replace(',', ' ') 71 | ret = ret.replace('[', ' ').replace(']', ' ') 72 | return ret 73 | 74 | def check_arg_type(obj_type, obj, field_name): 75 | is_ok = True 76 | warn_el_msg = 'Argument element in %s of type %s differs from alias %s' 77 | warn_msg = 'Argument %s of type %s differs from alias %s' 78 | try: 79 | if type(obj_type) == typing._GenericAlias: 80 | if obj_type.__args__: 81 | for arg_type in obj_type.__args__: 82 | for element in obj: 83 | curr_ok = arg_type == type(element) 84 | if not curr_ok: 85 | logger.warning(warn_el_msg%(field_name, element.__class__.__name__, arg_type.__name__)) 86 | is_ok &= curr_ok 87 | curr_ok = obj_type.__origin__ == type(obj) 88 | if not curr_ok: 89 | logger.warning(warn_msg%(field_name, obj_type.__class__.__name__, obj_type.__origin__)) 90 | return is_ok & curr_ok 91 | else: 92 | is_ok = type(obj) == obj_type 93 | if not is_ok: 94 | logger.warning(warn_msg%(field_name, type(obj).__name__, obj_type.__name__)) 95 | return is_ok 96 | except Exception as ex: 97 | handle_recursive_ex(ex, 'Error during check of argument type' + field_name) -------------------------------------------------------------------------------- /codegen/src/printer.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from .utils import get_valid_folder 3 | from .consts import PY_EXTENSION, TYPE_REFS_NAME, ENUMS_NAME, MUTATIONS_NAME, QUERIES_NAME, SCALARS_NAME, SIMPLE_TYPES_NAME, TEMPLATE_FOLDER, TYPES_NAME 4 | import os 5 | import logging as logger 6 | from .enums import TemplateType 7 | from .priority import ExtractionResults 8 | 9 | class Printer(): 10 | 11 | extractionResults: ExtractionResults 12 | 13 | def __init__(self, extractionResults) -> None: 14 | self.extraction_results = extractionResults 15 | 16 | def save_files(self, folder: str): 17 | try: 18 | if folder: 19 | folder = get_valid_folder(folder) 20 | else: 21 | logger.warning('Destination folder missing') 22 | self.save_file(TemplateType.SCALAR_TEMPLATE, str(pathlib.Path(folder, SCALARS_NAME + PY_EXTENSION).absolute()), 'scalar_defs') 23 | self.save_file(TemplateType.ENUM_TEMPLATE, str(pathlib.Path(folder, ENUMS_NAME + PY_EXTENSION).absolute()), 'enum_classes') 24 | self.save_file(TemplateType.FREE_TYPE_TEMPLATE, str(pathlib.Path(folder, SIMPLE_TYPES_NAME + PY_EXTENSION).absolute()), 'simple_type_classes') 25 | self.save_file(TemplateType.TYPE_TEMPLATE, str(pathlib.Path(folder, TYPES_NAME + PY_EXTENSION).absolute()), 'type_classes') 26 | self.save_operation_file(TemplateType.QUERY_TEMPLATE, str(pathlib.Path(folder, QUERIES_NAME + PY_EXTENSION).absolute()), ('query_classes', 'queries_enum_class')) 27 | self.save_operation_file(TemplateType.MUTATION_TEMPLATE, str(pathlib.Path(folder, MUTATIONS_NAME + PY_EXTENSION).absolute()), ('mutation_classes', 'mutations_enum_class')) 28 | self.save_file(TemplateType.TYPE_REFS_TEMPLATE, str(pathlib.Path(folder, TYPE_REFS_NAME + PY_EXTENSION).absolute()), 'type_refs') 29 | except Exception as ex: 30 | raise ex 31 | 32 | def save_file(self, enum_template, file_name, attr_name_result): 33 | """For internal use""" 34 | try: 35 | with open(file_name, 'w', encoding='UTF-8') as wrapper: 36 | wrapper.write(self.load_template_code(enum_template.value)) 37 | wrapper.write('\n') 38 | for curr_class in getattr(self.extraction_results, attr_name_result).values(): 39 | self.write_class_code(curr_class, wrapper) 40 | except Exception as ex: 41 | raise ex 42 | 43 | def save_operation_file(self, enum_template, file_name, attr_name_results): 44 | """For internal use""" 45 | try: 46 | if not getattr(self.extraction_results, attr_name_results[0]): return 47 | 48 | with open(file_name, 'w', encoding='UTF-8') as wrapper: 49 | wrapper.write(self.load_template_code(enum_template.value)) 50 | wrapper.write('\n') 51 | for curr_class in getattr(self.extraction_results, attr_name_results[0]).values(): 52 | self.write_class_code(curr_class, wrapper) 53 | if getattr(self.extraction_results, attr_name_results[0]): 54 | wrapper.write('\n') 55 | for queriesEnum in getattr(self.extraction_results, attr_name_results[1]).values(): 56 | self.write_class_code(queriesEnum, wrapper) 57 | except Exception as ex: 58 | raise ex 59 | 60 | def write_class_code(self, classCode, wrapper): 61 | """For internal use""" 62 | try: 63 | wrapper.write('\n') 64 | wrapper.writelines("%s\n" % i for i in classCode) 65 | except Exception as ex: 66 | raise ex 67 | 68 | def load_template_code(self, templateName): 69 | """For internal use""" 70 | try: 71 | fileName = os.path.join(get_valid_folder(TEMPLATE_FOLDER), templateName) 72 | with(templateFile := open(fileName)): 73 | return templateFile.read() 74 | except Exception as ex: 75 | raise ex 76 | -------------------------------------------------------------------------------- /pygqlmap/network.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib import request 3 | from urllib.error import HTTPError 4 | import logging as logger 5 | from .src.builder import ResultBuilder 6 | from .src.enums import BuildingType 7 | 8 | def send_http_request(api_url, payload, httpHeaders): 9 | try: 10 | body = json.dumps(payload, indent=2).encode('ascii') 11 | req = request.Request(api_url, data=body) 12 | for http_header_key, http_header_val in httpHeaders.items(): 13 | req.add_header(http_header_key, http_header_val) 14 | 15 | with request.urlopen(req) as response: 16 | response.text = response.read().decode('utf-8') 17 | response.ok = response.status == 200 18 | return response 19 | except HTTPError as ex: 20 | raise Exception(str(ex.status) + ' - ' + ex.reason) 21 | except Exception as ex: 22 | raise Exception(str(ex.args[0])) 23 | 24 | class GQLResponse(): 25 | http_response = None 26 | json_response = None 27 | errors = None 28 | data = None 29 | result_obj = None 30 | log_progress: bool= None 31 | 32 | def __init__(self, response: str, log_progress: bool = False): 33 | self.http_response = response 34 | self.json_response = json.loads(response.text) 35 | self.log_progress = log_progress 36 | 37 | if isinstance(self.json_response, dict): 38 | if "errors" in self.json_response.keys(): 39 | self.errors = self.json_response["errors"] 40 | if "data" in self.json_response.keys(): 41 | self.data = self.json_response["data"] 42 | 43 | def map_gqldata_to_obj(self, mapped_py_obj, build_type: BuildingType = BuildingType.STANDARD): 44 | """Maps the json response to a python object and saves it in result_obj field 45 | 46 | Args: 47 | mappedPyObject (_type_, optional): python object mapped from a GraphQL type 48 | A reference can be found in the GQLOperation object created as .type 49 | build_sctype (BuildingType, optional): Options not yet implemented. Defaults to BuildingType.STANDARD. 50 | """ 51 | if hasattr(self, 'data') and self.data: 52 | if not hasattr(mapped_py_obj, '__dataclass_fields__'):#it is a primitive 53 | self.result_obj = self.data.popitem()[1] 54 | else: 55 | myBuilder = ResultBuilder(build_type, self.log_progress) 56 | self.result_obj = myBuilder.build(self.data, mapped_py_obj) 57 | else: 58 | self.result_obj = None 59 | 60 | def print_msg_out(self): 61 | """!This function works with built-in python module urllib3 and requests library 62 | 63 | Raises: 64 | Exception: HTTP status code != 200 65 | Exception: Presence of errors in the json response 66 | Exception: Missing errors and data in the json response 67 | """ 68 | if hasattr(self.http_response, 'status'): 69 | status = self.http_response.status 70 | else: 71 | status = self.http_response.status_code 72 | logger.info('Network result: ' + 'OK' if self.http_response.ok else 'KO') 73 | logger.info('HTTP code: ' + str(status)) 74 | 75 | if status != 200: 76 | error = 'HTTP Error: ' + self.http_response.text 77 | logger.error(error) 78 | raise Exception(error) 79 | else: 80 | if hasData :=(hasattr(self, 'data') and self.data): 81 | logger.info('Data returned: ') 82 | logger.info(json.dumps(self.data, indent=2)) 83 | 84 | if hasErrors :=(hasattr(self, 'errors') and self.errors): 85 | err_out = '' 86 | for error in self.errors: 87 | err_out += str(error) + '\n' 88 | err_out += '\n' 89 | logger.error('Response Errors: ' + err_out) 90 | 91 | if not hasData and not hasErrors: 92 | raise Exception('Inconsistent response: ' + self.http_response.text) 93 | -------------------------------------------------------------------------------- /tests/output/re/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class ApprovalStatus(Enum): 4 | DEFAULT = None 5 | APPROVAL_NOT_REQUIRED = 'APPROVAL_NOT_REQUIRED' 6 | EVIDENCE_NEEDED = 'EVIDENCE_NEEDED' 7 | EVIDENCE_PENDING_APPROVAL = 'EVIDENCE_PENDING_APPROVAL' 8 | EVIDENCE_APPROVED = 'EVIDENCE_APPROVED' 9 | 10 | class MembersMemberConsumptionEvidenceSubmissionStatusChoices(Enum): 11 | DEFAULT = None 12 | EVIDENCE_NEEDED = 'EVIDENCE_NEEDED' ##Evidence needed 13 | EVIDENCE_PENDING_APPROVAL = 'EVIDENCE_PENDING_APPROVAL' ##Evidence pending approval 14 | EVIDENCE_APPROVED = 'EVIDENCE_APPROVED' ##Evidence approved 15 | 16 | class GenerationGenerationFarmGenerationTypeChoices(Enum): 17 | DEFAULT = None 18 | WIND = 'WIND' ##Windfarm 19 | SOL = 'SOL' ##Solarfarm 20 | 21 | class GenerationGenerationFarmOperationalStatusChoices(Enum): 22 | DEFAULT = None 23 | NA = 'NA' ##N/A 24 | PRELIMINARY = 'PRELIMINARY' ##Prelimary 25 | PLANNING = 'PLANNING' ##Planning 26 | CONTRACTED = 'CONTRACTED' ##Contracted 27 | UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION' ##Under construction 28 | OPERATIONAL = 'OPERATIONAL' ##Operational 29 | SUSPENDED = 'SUSPENDED' ##Suspended 30 | DECOMMISSIONED = 'DECOMMISSIONED' ##Decommissioned 31 | CLOSED = 'CLOSED' ##Closed 32 | 33 | class GenerationDataTitle(Enum): 34 | DEFAULT = None 35 | TODAY = 'TODAY' 36 | NEXT_4_DAYS = 'NEXT_4_DAYS' 37 | NEXT_14_DAYS = 'NEXT_14_DAYS' 38 | LAST_7_DAYS = 'LAST_7_DAYS' 39 | LAST_30_DAYS = 'LAST_30_DAYS' 40 | 41 | class Period(Enum): 42 | DEFAULT = None 43 | Day = 'Day' 44 | Week = 'Week' 45 | Month = 'Month' 46 | Year = 'Year' 47 | 48 | class CoopCoopStatusChoices(Enum): 49 | DEFAULT = None 50 | RESERVATIONS_OPEN = 'RESERVATIONS_OPEN' ##Reservations open 51 | RESERVATIONS_CLOSED = 'RESERVATIONS_CLOSED' ##Reservations closed 52 | OWNERSHIP_OPEN_RESERVED_MEMBERS_ONLY = 'OWNERSHIP_OPEN_RESERVED_MEMBERS_ONLY' ##Ownership open reserved members only 53 | OWNERSHIP_OPEN = 'OWNERSHIP_OPEN' ##Ownership open 54 | FULL = 'FULL' ##Full 55 | BACKFILL = 'BACKFILL' ##Backfill 56 | CLOSED = 'CLOSED' ##Closed 57 | 58 | class PaymentsPaymentCategoryChoices(Enum): 59 | DEFAULT = None 60 | JOINING_FEE = 'JOINING_FEE' ##Ripple joining fee 61 | GIFT_VOUCHER = 'GIFT_VOUCHER' ##Gift Voucher 62 | ORDER = 'ORDER' ##Order 63 | 64 | class InvoiceType(Enum): 65 | DEFAULT = None 66 | RESERVATION_RECEIPT = 'RESERVATION_RECEIPT' 67 | WATTAGE_AMOUNT = 'WATTAGE_AMOUNT' 68 | ARRANGEMENT_FEE = 'ARRANGEMENT_FEE' 69 | GIFT_VOUCHER = 'GIFT_VOUCHER' 70 | INVOICE = 'INVOICE' 71 | 72 | class PaymentType(Enum): 73 | DEFAULT = None 74 | RESERVATION_FEE = 'RESERVATION_FEE' 75 | GIFT_VOUCHER = 'GIFT_VOUCHER' 76 | MONTHLY_INSTALMENT = 'MONTHLY_INSTALMENT' 77 | INITIAL_INSTALMENT = 'INITIAL_INSTALMENT' 78 | ONE_TIME_PAYMENT = 'ONE_TIME_PAYMENT' 79 | PAYMENT = 'PAYMENT' 80 | 81 | class PaymentStatus(Enum): 82 | DEFAULT = None 83 | PAID = 'PAID' 84 | FAILED = 'FAILED' 85 | REFUNDED = 'REFUNDED' 86 | UPCOMING = 'UPCOMING' 87 | OVERDUE = 'OVERDUE' 88 | 89 | class CrmUserMemoChannelChoices(Enum): 90 | DEFAULT = None 91 | DIRECT_EMAIL = 'DIRECT_EMAIL' ##DIRECT_EMAIl 92 | INFO = 'INFO' ##INFO 93 | HELP = 'HELP' ##HELP 94 | CONTACT_US = 'CONTACT_US' ##CONTACT_US 95 | EMPLOYER = 'EMPLOYER' ##EMPLOYER 96 | BUSINESS = 'BUSINESS' ##BUSINESS 97 | OTHER = 'OTHER' ##OTHER 98 | 99 | class GetMemberOctopusConsumptionDataTitle(Enum): 100 | DEFAULT = None 101 | LAST_7_DAYS = 'LAST_7_DAYS' 102 | LAST_30_DAYS = 'LAST_30_DAYS' 103 | 104 | class SurveyQuestionChoiceTypeChoices(Enum): 105 | DEFAULT = None 106 | MULTIPLE_CHOICE = 'MULTIPLE_CHOICE' ##multiple choice 107 | YES_NO = 'YES_NO' ##yes no 108 | RADIO = 'RADIO' ##radio 109 | TEXT = 'TEXT' ##text 110 | NUMBER = 'NUMBER' ##number 111 | 112 | class StripePaymentType(Enum): 113 | DEFAULT = None 114 | CARD = 'CARD' 115 | 116 | class CardBrandType(Enum): 117 | DEFAULT = None 118 | OTHER = 'OTHER' 119 | VISA = 'VISA' 120 | MASTERCARD = 'MASTERCARD' 121 | -------------------------------------------------------------------------------- /tests/output/re_nodesc/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class ApprovalStatus(Enum): 4 | DEFAULT = None 5 | APPROVAL_NOT_REQUIRED = 'APPROVAL_NOT_REQUIRED' 6 | EVIDENCE_NEEDED = 'EVIDENCE_NEEDED' 7 | EVIDENCE_PENDING_APPROVAL = 'EVIDENCE_PENDING_APPROVAL' 8 | EVIDENCE_APPROVED = 'EVIDENCE_APPROVED' 9 | 10 | class MembersMemberConsumptionEvidenceSubmissionStatusChoices(Enum): 11 | DEFAULT = None 12 | EVIDENCE_NEEDED = 'EVIDENCE_NEEDED' ##Evidence needed 13 | EVIDENCE_PENDING_APPROVAL = 'EVIDENCE_PENDING_APPROVAL' ##Evidence pending approval 14 | EVIDENCE_APPROVED = 'EVIDENCE_APPROVED' ##Evidence approved 15 | 16 | class GenerationGenerationFarmGenerationTypeChoices(Enum): 17 | DEFAULT = None 18 | WIND = 'WIND' ##Windfarm 19 | SOL = 'SOL' ##Solarfarm 20 | 21 | class GenerationGenerationFarmOperationalStatusChoices(Enum): 22 | DEFAULT = None 23 | NA = 'NA' ##N/A 24 | PRELIMINARY = 'PRELIMINARY' ##Prelimary 25 | PLANNING = 'PLANNING' ##Planning 26 | CONTRACTED = 'CONTRACTED' ##Contracted 27 | UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION' ##Under construction 28 | OPERATIONAL = 'OPERATIONAL' ##Operational 29 | SUSPENDED = 'SUSPENDED' ##Suspended 30 | DECOMMISSIONED = 'DECOMMISSIONED' ##Decommissioned 31 | CLOSED = 'CLOSED' ##Closed 32 | 33 | class GenerationDataTitle(Enum): 34 | DEFAULT = None 35 | TODAY = 'TODAY' 36 | NEXT_4_DAYS = 'NEXT_4_DAYS' 37 | NEXT_14_DAYS = 'NEXT_14_DAYS' 38 | LAST_7_DAYS = 'LAST_7_DAYS' 39 | LAST_30_DAYS = 'LAST_30_DAYS' 40 | 41 | class Period(Enum): 42 | DEFAULT = None 43 | Day = 'Day' 44 | Week = 'Week' 45 | Month = 'Month' 46 | Year = 'Year' 47 | 48 | class CoopCoopStatusChoices(Enum): 49 | DEFAULT = None 50 | RESERVATIONS_OPEN = 'RESERVATIONS_OPEN' ##Reservations open 51 | RESERVATIONS_CLOSED = 'RESERVATIONS_CLOSED' ##Reservations closed 52 | OWNERSHIP_OPEN_RESERVED_MEMBERS_ONLY = 'OWNERSHIP_OPEN_RESERVED_MEMBERS_ONLY' ##Ownership open reserved members only 53 | OWNERSHIP_OPEN = 'OWNERSHIP_OPEN' ##Ownership open 54 | FULL = 'FULL' ##Full 55 | BACKFILL = 'BACKFILL' ##Backfill 56 | CLOSED = 'CLOSED' ##Closed 57 | 58 | class PaymentsPaymentCategoryChoices(Enum): 59 | DEFAULT = None 60 | JOINING_FEE = 'JOINING_FEE' ##Ripple joining fee 61 | GIFT_VOUCHER = 'GIFT_VOUCHER' ##Gift Voucher 62 | ORDER = 'ORDER' ##Order 63 | 64 | class InvoiceType(Enum): 65 | DEFAULT = None 66 | RESERVATION_RECEIPT = 'RESERVATION_RECEIPT' 67 | WATTAGE_AMOUNT = 'WATTAGE_AMOUNT' 68 | ARRANGEMENT_FEE = 'ARRANGEMENT_FEE' 69 | GIFT_VOUCHER = 'GIFT_VOUCHER' 70 | INVOICE = 'INVOICE' 71 | 72 | class PaymentType(Enum): 73 | DEFAULT = None 74 | RESERVATION_FEE = 'RESERVATION_FEE' 75 | GIFT_VOUCHER = 'GIFT_VOUCHER' 76 | MONTHLY_INSTALMENT = 'MONTHLY_INSTALMENT' 77 | INITIAL_INSTALMENT = 'INITIAL_INSTALMENT' 78 | ONE_TIME_PAYMENT = 'ONE_TIME_PAYMENT' 79 | PAYMENT = 'PAYMENT' 80 | 81 | class PaymentStatus(Enum): 82 | DEFAULT = None 83 | PAID = 'PAID' 84 | FAILED = 'FAILED' 85 | REFUNDED = 'REFUNDED' 86 | UPCOMING = 'UPCOMING' 87 | OVERDUE = 'OVERDUE' 88 | 89 | class CrmUserMemoChannelChoices(Enum): 90 | DEFAULT = None 91 | DIRECT_EMAIL = 'DIRECT_EMAIL' ##DIRECT_EMAIl 92 | INFO = 'INFO' ##INFO 93 | HELP = 'HELP' ##HELP 94 | CONTACT_US = 'CONTACT_US' ##CONTACT_US 95 | EMPLOYER = 'EMPLOYER' ##EMPLOYER 96 | BUSINESS = 'BUSINESS' ##BUSINESS 97 | OTHER = 'OTHER' ##OTHER 98 | 99 | class GetMemberOctopusConsumptionDataTitle(Enum): 100 | DEFAULT = None 101 | LAST_7_DAYS = 'LAST_7_DAYS' 102 | LAST_30_DAYS = 'LAST_30_DAYS' 103 | 104 | class SurveyQuestionChoiceTypeChoices(Enum): 105 | DEFAULT = None 106 | MULTIPLE_CHOICE = 'MULTIPLE_CHOICE' ##multiple choice 107 | YES_NO = 'YES_NO' ##yes no 108 | RADIO = 'RADIO' ##radio 109 | TEXT = 'TEXT' ##text 110 | NUMBER = 'NUMBER' ##number 111 | 112 | class StripePaymentType(Enum): 113 | DEFAULT = None 114 | CARD = 'CARD' 115 | 116 | class CardBrandType(Enum): 117 | DEFAULT = None 118 | OTHER = 'OTHER' 119 | VISA = 'VISA' 120 | MASTERCARD = 'MASTERCARD' 121 | -------------------------------------------------------------------------------- /codegen/query_presets.py: -------------------------------------------------------------------------------- 1 | QUERY_TYPE_REF = """ 2 | fragment TypeRef on __Type { 3 | kind 4 | name 5 | ofType { 6 | kind 7 | name 8 | ofType { 9 | kind 10 | name 11 | ofType { 12 | kind 13 | name 14 | ofType { 15 | kind 16 | name 17 | ofType { 18 | kind 19 | name 20 | ofType { 21 | kind 22 | name 23 | ofType { 24 | kind 25 | name 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }""" 34 | 35 | QUERY_TYPE_DETAILS = """ 36 | fragment FullType on __Type { 37 | kind 38 | name 39 | description 40 | fields(includeDeprecated: true) { 41 | name 42 | description 43 | args { ...InputValue } 44 | type { ...TypeRef } 45 | isDeprecated 46 | deprecationReason 47 | } 48 | inputFields { ...InputValue } 49 | interfaces { ...TypeRef } 50 | enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } 51 | possibleTypes { ...TypeRef } 52 | } 53 | 54 | fragment InputValue on __InputValue { 55 | name 56 | description 57 | type { ...TypeRef } 58 | defaultValue 59 | } 60 | """ + QUERY_TYPE_REF 61 | 62 | QUERY_TYPE = "query { __type(name: \"%s\") { name kind description fields { name } } }" 63 | 64 | QUERY_SCHEMA_BASIC = """query IntrospectionQuery { 65 | __schema { 66 | queryType { name } 67 | mutationType { name } 68 | subscriptionType { name } 69 | types { kind name description } 70 | directives { 71 | name 72 | description 73 | locations 74 | args { name description defaultValue } 75 | } 76 | } 77 | }""" 78 | 79 | QUERY_SCHEMA_MUTATION_TYPE = """query { 80 | __schema { 81 | mutationType { 82 | name 83 | fields { 84 | name 85 | args { 86 | name 87 | defaultValue 88 | type { 89 | ...TypeRef 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | """ + QUERY_TYPE_REF 97 | 98 | QUERY_SCHEMA_AND_TYPES = """query IntrospectionQuery { 99 | __schema { 100 | queryType { name } 101 | mutationType { name } 102 | subscriptionType { name } 103 | types { ...FullType } 104 | directives { 105 | name 106 | description 107 | locations 108 | args { ...InputValue } 109 | } 110 | } 111 | } 112 | """ + QUERY_TYPE_DETAILS 113 | 114 | QUERY_TYPE_MUTATION = """query introspectionMutationType { 115 | __type(name: "Mutation") { 116 | ...FullType 117 | } 118 | } 119 | """ + QUERY_TYPE_DETAILS 120 | -------------------------------------------------------------------------------- /tests/tstquery/manual_query_fragment_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | USE CASE DESCRIPTION: 3 | This test shows how to simulate a fragment behavior creating a manual python object as fragment and integrate it in multiple queries from the generated code. 4 | 5 | Fragment to reproduce: 6 | 7 | currency { 8 | code 9 | symbol 10 | } 11 | 12 | Queries to reproduce: 13 | 14 | query1 fraggedAllCoops { 15 | allCoops { 16 | id 17 | currency { #python CurrencyFragment "fragment" 18 | code 19 | symbol 20 | } 21 | name 22 | code 23 | generationfarm { 24 | id 25 | currency { #python CurrencyFragment "fragment" 26 | code 27 | symbol 28 | } 29 | name 30 | } 31 | } 32 | } 33 | 34 | query2 fraggedCurrencies { 35 | currency { #python CurrencyFragment "fragment" 36 | code 37 | symbol 38 | } 39 | } 40 | STEP 1: Define the python class corresponding to the GraphQL fragment you intend to reuse 41 | """ 42 | ##STEP 1 43 | from pygqlmap.components import GQLObject 44 | import logging as logger 45 | import requests 46 | 47 | from ..utils import stringifyresult 48 | 49 | class CurrencyFragment(GQLObject): 50 | code: str 51 | symbol: str 52 | 53 | ## 54 | 55 | """ 56 | STEP 2: Instantiate the first generated query integrating the fragment 57 | STEP 3: Execute the first query and parse response 58 | STEP 4: Instantiate the second generated query integrating the fragment 59 | STEP 5: Execute the first query and parse response 60 | 61 | RESULT: The python class has been used as fragment 62 | """ 63 | from ..consts import GITHUB_URL, GITHUB_HEADERS, RE_HEADERS, RE_URL 64 | 65 | from pygqlmap.network import GQLResponse 66 | from tests.output.re_nodesc.queries import allCoops, currencies 67 | 68 | def run_manual_query_fragment(): 69 | logger.debug('\n\nRunning run_manual_query_fragment...') 70 | try: 71 | ##STEP 2 72 | query1 = allCoops() 73 | query1.name = 'fraggedAllCoops' 74 | query1.type.currency = CurrencyFragment() 75 | query1.type.generationfarm.currency = CurrencyFragment() 76 | 77 | #remove not shown fields for the example 78 | query1.set_show("allCoops.*", False) 79 | query1.set_show("allCoops.id", True) 80 | query1.set_show("allCoops.currency", True) 81 | query1.set_show("allCoops.name", True) 82 | query1.set_show("allCoops.code", True) 83 | query1.set_show("allCoops.generationfarm.*", False) 84 | query1.set_show("allCoops.generationfarm.id", True) 85 | query1.set_show("allCoops.generationfarm.currency", True) 86 | query1.set_show("allCoops.generationfarm.name", True) 87 | 88 | 89 | ## 90 | 91 | ##STEP 3 92 | response = requests.request('POST', url=RE_URL, 93 | json={ "query": query1.export_gql_source }, 94 | headers=RE_HEADERS) 95 | 96 | gqlResponse1 = GQLResponse(response) 97 | ## 98 | 99 | ##STEP 4 100 | query2 = currencies() 101 | query2.name = 'fraggedCurrencies' 102 | query2.type = CurrencyFragment() 103 | ## 104 | 105 | ##STEP 5 106 | response2 = requests.request('POST', url=RE_URL, 107 | json={ "query": query2.export_gql_source }, 108 | headers=RE_HEADERS) 109 | 110 | gqlResponse2 = GQLResponse(response) 111 | ## 112 | 113 | ##RESULT 114 | logger.debug('Query GQL syntax: ' + query1.export_gql_source) 115 | gqlResponse1.print_msg_out() 116 | gqlResponse1.map_gqldata_to_obj(query1.type) 117 | logger.info('result object 1: ' + stringifyresult(gqlResponse1.result_obj)) 118 | logger.debug('Query GQL syntax: ' + query2.export_gql_source) 119 | gqlResponse2.print_msg_out() 120 | gqlResponse2.map_gqldata_to_obj(query2.type) 121 | logger.info('result object 2: ' + stringifyresult(gqlResponse2.result_obj)) 122 | ## 123 | except Exception as ex: 124 | raise ex 125 | 126 | logger.debug("End of run_manual_query_fragment") 127 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import pathlib 4 | 5 | 6 | sys.path.append(str(pathlib.Path(os.path.dirname(sys.path[0])).absolute())) 7 | sys.path.append(str(pathlib.Path(os.path.dirname(sys.path[0]), 'test').absolute())) 8 | 9 | import unittest 10 | from .help_unittest import run_generator_cmd_help 11 | from .help_unittest import run_generator_cmd_help 12 | import logging as logger 13 | 14 | 15 | class TestCLI(unittest.TestCase): 16 | 17 | def test_generator_cmd_help(self): 18 | return run_generator_cmd_help() 19 | 20 | def test_download_cmd_ra_file_rel(self): 21 | return run_download_cmd_ra_file_rel() 22 | 23 | def test_generate_cmd_ra_api_abs(self): 24 | return run_generate_cmd_ra_api_abs() 25 | 26 | def test_download_cmd_gdbc_file_rel(self): 27 | return run_download_cmd_gdbc_file_rel() 28 | 29 | def test_generate_cmd_gdbc_file_rel(self): 30 | return run_generate_cmd_gdbc_file_rel() 31 | 32 | def test_download_cmd_gh_file_rel(self): 33 | return run_download_cmd_gh_file_rel() 34 | 35 | def test_generate_cmd_gh_api_abs(self): 36 | return run_generate_cmd_gh_api_abs() 37 | 38 | def test_generate_cmd_gh_file(self): 39 | return run_generate_cmd_gh_file() 40 | 41 | 42 | def run_download_cmd_ra_file_rel(): 43 | logger.debug('\nRunning run_download_cmd_ra_file_rel...') 44 | command = "pgmcodegen download ./tests/cmd_output/rapidapi/schema.json -apiArgs ./tests/cli_input/rapid_api/downloaderArgs.json -v" #command to be executed 45 | logger.debug("Launching: " + command) 46 | 47 | res = os.system(command) 48 | assert res == 0 49 | logger.debug("End of run_download_cmd_ra_file_rel") 50 | 51 | def run_generate_cmd_ra_api_abs(): 52 | logger.debug('\nRunning run_generate_cmd_ra_api_abs...') 53 | command = "pgmcodegen generate ./tests/cmd_output/rapidapi -v -apiArgs ./tests/cli_input/rapid_api/generatorArgs.json -v" #command to be executed 54 | logger.debug("Launching: " + command) 55 | 56 | res = os.system(command) 57 | assert res == 0 58 | logger.debug("End of run_generate_cmd_ra_api_abs") 59 | 60 | def run_download_cmd_gdbc_file_rel(): 61 | logger.debug('\nRunning run_download_cmd_gh_file_rel...') 62 | command = "pgmcodegen download ./tests/cmd_output/gdbc/schema.json -apiArgs ./tests/cli_input/gdbc_api/downloaderArgs.json -v" #command to be executed 63 | logger.debug("Launching: " + command) 64 | 65 | res = os.system(command) 66 | assert res == 0 67 | logger.debug("End of run_download_cmd_gh_file_rel") 68 | 69 | def run_generate_cmd_gdbc_file_rel(): 70 | logger.debug('\nRunning run_generate_cmd_gdbc_file_rel...') 71 | command = "pgmcodegen generate ./tests/cmd_output/gdbc -apiArgs tests/cli_input/gdbc_api/schema.json -v" #command to be executed 72 | logger.debug("Launching: " + command) 73 | 74 | res = os.system(command) 75 | assert res == 0 76 | logger.debug("End of run_generate_cmd_gdbc_file_rel") 77 | 78 | def run_generate_cmd_gdbc_api_rel(): 79 | logger.debug('\nRunning run_generate_cmd_gdbc_api_rel...') 80 | command = "pgmcodegen generate ./tests/cmd_output/gdbc -apiArgs ./tests/cli_input/gdbc_api/generatorArgs.json -v" #command to be executed 81 | logger.debug("Launching: " + command) 82 | 83 | res = os.system(command) 84 | assert res == 0 85 | logger.debug("End of run_generate_cmd_gdbc_api_rel") 86 | 87 | 88 | def run_download_cmd_gh_file_rel(): 89 | logger.debug('\nRunning run_download_cmd_gh_file_rel...') 90 | command = "pgmcodegen download ./tests/cmd_output/github/schema.json -apiArgs ./tests/cli_input/gh_api/downloaderArgs.json -v" #command to be executed 91 | logger.debug("Launching: " + command) 92 | 93 | res = os.system(command) 94 | assert res == 0 95 | logger.debug("End of run_download_cmd_gh_file_rel") 96 | 97 | def run_generate_cmd_gh_api_abs(): 98 | logger.debug('\nRunning run_generate_cmd_gh_api_abs...') 99 | command = "pgmcodegen generate ./tests/cmd_output/github -v -apiArgs ./tests/cli_input/gh_api/generatorArgs.json -v" #command to be executed 100 | logger.debug("Launching: " + command) 101 | 102 | res = os.system(command) 103 | assert res == 0 104 | logger.debug("End of run_generate_cmd_gh_api_abs") 105 | 106 | def run_generate_cmd_gh_file(): 107 | logger.debug('\nRunning run_generate_cmd_gh_api_abs...') 108 | command = "pgmcodegen generate ./tests/cmd_output/github -v -apiArgs ./tests/cli_input/gh_api/generatorArgs.json -v" #command to be executed 109 | logger.debug("Launching: " + command) 110 | 111 | res = os.system(command) 112 | assert res == 0 113 | logger.debug("End of run_generate_cmd_gh_api_abs") 114 | -------------------------------------------------------------------------------- /tests/tstmutation/mutation_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Mutation using arguments as Literal Values to UPDATE a GraphQL record and build the python class instance containing the data from the response 5 | 6 | Mutation to reproduce: 7 | 8 | mutation myManualUpdateRepository { 9 | updateRepository ( input: { repositoryId: "R_kgDOH7MI4g", hasIssuesEnabled: false } ) 10 | { 11 | clientMutationId 12 | repository { 13 | allowUpdateBranch 14 | assignableUsers(first: 1) { 15 | edges { 16 | cursor 17 | node { 18 | anyPinnableItems 19 | avatarUrl 20 | bio 21 | bioHTML 22 | } 23 | } 24 | totalCount 25 | pageInfo { 26 | startCursor 27 | endCursor 28 | hasNextPage 29 | hasPreviousPage 30 | } 31 | } 32 | autoMergeAllowed 33 | codeowners { 34 | errors 35 | { 36 | column 37 | kind 38 | line 39 | message 40 | path 41 | source 42 | suggestion 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | 50 | STEP 1: Instantiate GQLOperation class representing the GraphQL mutation 51 | STEP 2: Instantiate GQLArgs object structure with argument type as LITERAL_VALUES 52 | STEP 3: Query the GraphQL server 53 | STEP 4: Pass the response received to the GQLResponse constructor 54 | STEP 5: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 55 | 56 | RESULT: The object within gqlResponse.result_obj will contain the data from the GraphQL server 57 | """ 58 | 59 | import requests 60 | from ..consts import GITHUB_URL, GITHUB_HEADERS 61 | from ..output.github.mutations import updateRepository 62 | from ..output.github.gql_types import Package, UpdateRepositoryInput 63 | import logging as logger 64 | from ..utils import stringifyresult 65 | 66 | def run_gh_update_mutation_literal(): 67 | logger.debug('\n\nRunning run_gh_update_mutation_literal...') 68 | ##STEP 2 69 | mutation = updateRepository() 70 | mutation.name = 'myManualUpdateRepository' 71 | ## 72 | 73 | ##STEP 3 74 | mutationInput = UpdateRepositoryInput() 75 | mutationInput.repositoryId = "R_kgDOH7MI4g" 76 | mutationInput.hasIssuesEnabled = False 77 | mutation._args.input = mutationInput 78 | 79 | mutation.type.repository.assignableUsers._args.first = 5 80 | 81 | mutation.type.repository.branchProtectionRules = type(mutation.type.repository.branchProtectionRules)(number=1, first=1, after='') 82 | mutation.type.repository.discussion._args.number = 1 83 | mutation.type.repository.discussions = type(mutation.type.repository.discussions)(first=1, after='') 84 | mutation.type.repository.discussionCategory._args.slug = 'slug' 85 | mutation.type.repository.discussionCategories = type(mutation.type.repository.discussionCategories)(first=1, after='') 86 | mutation.type.repository.environment._args.name = 'envName' 87 | mutation.type.repository.label._args.name = 'lblName' 88 | mutation.type.repository.milestone._args.number = 2 89 | package = Package() 90 | package.version._args.version = '1.1.1' 91 | mutation.type.repository.packages.nodes = [package] 92 | mutation.type.repository.project._args.number = 1 93 | mutation.type.repository.refs._args.refPrefix = 'ref' 94 | mutation.type.repository.release._args.tagName = 'tagX' 95 | 96 | mutation.type.repository.packages.edges.node.version._args.version = '1' 97 | mutation.type.repository.deployKeys._args.first = 3 98 | mutation.type.repository.deployments._args.first = 5 99 | 100 | 101 | try: 102 | logger.debug('Query GQL syntax: ' + mutation.export_gql_source) 103 | ## 104 | 105 | ##STEP 4 106 | response = requests.request('POST', url=GITHUB_URL, 107 | json={ "query": mutation.export_gql_source }, 108 | headers=GITHUB_HEADERS) 109 | ## 110 | 111 | ##STEP 5 112 | from pygqlmap.network import GQLResponse 113 | 114 | gqlResponse = GQLResponse(response) 115 | ## 116 | 117 | gqlResponse.print_msg_out() 118 | 119 | ##STEP 6 120 | gqlResponse.map_gqldata_to_obj(mutation.type) 121 | ## 122 | 123 | ##RESULT 124 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 125 | ## 126 | except Exception as ex: 127 | raise ex 128 | 129 | logger.debug("End of run_gh_update_mutation_literal") 130 | -------------------------------------------------------------------------------- /tests/tstquery/complex_obj_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | USE CASE DESCRIPTION: 3 | This test shows how to create a GraphQL Query to fetch a GraphQL complex type (made of objects, connections, and scalar fields) combining arguments usage as literal values 4 | with visibility usage for chosen fields, and finally build the python class instance containing the data from the response 5 | 6 | Query to reproduce: 7 | 8 | query myCountriesQuery { 9 | countries { 10 | edges { 11 | cursor 12 | node { 13 | code 14 | callingCode (*) 15 | wikiDataId 16 | capital 17 | name 18 | currencyCodes (*) 19 | flagImageUri 20 | numRegions 21 | region { 22 | fipsCode (*) 23 | isoCode (*) 24 | wikiDataId (*) 25 | name 26 | capital (*) 27 | country { (*) 28 | code 29 | callingCode 30 | wikiDataId 31 | capital 32 | name 33 | currencyCodes 34 | flagImageUri 35 | numRegions 36 | } 37 | populatedPlaces { (*) 38 | totalCount 39 | } 40 | } 41 | regions { 42 | edges { 43 | cursor 44 | } 45 | totalCount 46 | pageInfo { 47 | startCursor 48 | endCursor 49 | hasNextPage (*) 50 | hasPreviousPage 51 | } 52 | } 53 | } 54 | } 55 | totalCount 56 | pageInfo { 57 | startCursor 58 | endCursor 59 | hasNextPage (*) 60 | hasPreviousPage 61 | } 62 | } 63 | } 64 | 65 | 66 | STEP 1: Instantiate the class representing the GraphQL query 67 | STEP 2: Set the arguments 68 | STEP 3: Call set_show function of GQLOperation class to set the visibility of chosen fields (path to declare with dot notation) 69 | STEP 4: Query the GraphQL server 70 | STEP 5: Pass the response received to the GQLResponse constructor 71 | STEP 6: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 72 | 73 | RESULT: 74 | a) The request toward the GraphQL server will not have the hidden fields 75 | b) The request toward the GraphQL server will have the query with arguments 'currencyCode' and 'code' 76 | b) The python class instance obtained from the response will not have the hidden fields 77 | """ 78 | 79 | import requests 80 | from ..consts import GDBC_HEADERS, GDBC_URL 81 | # from ..utils import ManageException 82 | from ..output.gdbc.queries import countries 83 | import logging as logger 84 | from ..utils import stringifyresult 85 | 86 | 87 | def run_gdbc_complex_obj(): 88 | logger.debug('\n\nRunning test_gdbc_complex_obj...') 89 | ##STEP 1 90 | query = countries() 91 | query.name = 'myCountriesQuery' 92 | ## 93 | 94 | ##STEP 2 95 | query._args.currencyCode = 'USD' 96 | query.type.edges.node.region._args.code = "AK" 97 | ## 98 | 99 | ##STEP 3 100 | query.set_show('countries.edges.node.callingCode', False) 101 | query.set_show('countries.edges.node.currencyCodes', False) 102 | query.set_show('countries.edges.node.regions.pageInfo.hasNextPage', False) 103 | query.set_show('countries.edges.node.region.*', False) 104 | query.set_show('countries.edges.node.region.name', True) 105 | query.set_show('countries.pageInfo.hasNextPage', False) 106 | ## 107 | 108 | ##RESULT a) and b) 109 | print('Query GQL syntax: ' + query.export_gql_source) 110 | ## 111 | 112 | ##STEP 4 113 | try: 114 | response = requests.request('POST', url=GDBC_URL, 115 | json={ "query": query.export_gql_source }, 116 | headers=GDBC_HEADERS) 117 | ## 118 | 119 | ##STEP 5 120 | from pygqlmap.network import GQLResponse 121 | 122 | gqlResponse = GQLResponse(response) 123 | ## 124 | 125 | gqlResponse.print_msg_out() 126 | 127 | ##STEP 6 128 | gqlResponse.map_gqldata_to_obj(query.type) 129 | ## 130 | 131 | ##RESULT c) 132 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 133 | ## 134 | 135 | except Exception as ex: 136 | raise ex 137 | 138 | logger.debug("End of test_gdbc_complex_obj") 139 | -------------------------------------------------------------------------------- /tests/re_unittest.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from codegen.network import fetch_schema_obj 3 | from codegen.generator import CodeGenerator 4 | from codegen.query_presets import QUERY_SCHEMA_AND_TYPES 5 | from pygqlmap.enums import ArgType 6 | from pygqlmap.network import GQLResponse 7 | from tests.output.re_nodesc.enums import Period 8 | from tests.output.re_nodesc.gql_simple_types import InsightsChartDataInput 9 | from tests.output.re_nodesc.gql_types import JFUJX_InsightsChartDataOutput_Field 10 | from tests.output.re_nodesc.queries import allCoops, branding, version 11 | from tests.utils import stringifyresult 12 | from .consts import RE_HEADERS, RE_URL 13 | import logging as logger 14 | 15 | def run_fetch_re_schema(): 16 | logger.debug('\n\nRunning run_fetch_re_schema...') 17 | 18 | try: 19 | gqlSchema = fetch_schema_obj(RE_URL, RE_HEADERS, QUERY_SCHEMA_AND_TYPES) 20 | 21 | if gqlSchema: 22 | logger.debug('Generating python types from GraphQL data...') 23 | CodeGenerator.generate_code(gqlSchema, folder='tests\\output\\re\\', log_progress=True) 24 | logger.debug('Python types generated') 25 | 26 | except Exception as ex: 27 | raise ex 28 | 29 | logger.debug("End of run_fetch_re_schema") 30 | 31 | def run_fetch_re_schema_no_desc(): 32 | logger.debug('\n\nRunning run_fetch_re_schema_no_desc...') 33 | 34 | try: 35 | gqlSchema = fetch_schema_obj(RE_URL, RE_HEADERS, QUERY_SCHEMA_AND_TYPES) 36 | 37 | if gqlSchema: 38 | logger.debug('Generating python types from GraphQL data...') 39 | CodeGenerator.generate_code(gqlSchema, folder='tests\\output\\re_nodesc\\', add_desc=False, log_progress=True) 40 | logger.debug('Python types generated') 41 | except Exception as ex: 42 | raise ex 43 | 44 | logger.debug("End of run_fetch_gdbc_schema_no_desc") 45 | 46 | def run_re_version_literal(): 47 | logger.debug('\n\nRunning run_re_version_literal...') 48 | query = version() 49 | query.name = 'myVersionQuery' 50 | 51 | query._args_type = ArgType.LITERAL_VALUES 52 | 53 | try: 54 | logger.debug('gqlSource GQL version: ' + query.export_gql_source) 55 | 56 | response = requests.request('POST', url=RE_URL, 57 | json={ "query": query.export_gql_source }, 58 | headers=RE_HEADERS) 59 | gqlResponse = GQLResponse(response) 60 | 61 | gqlResponse.print_msg_out() 62 | gqlResponse.map_gqldata_to_obj(query.type) 63 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 64 | except Exception as ex: 65 | raise ex 66 | 67 | logger.debug("End of run_re_version_literal") 68 | 69 | def run_re_version_vars(): 70 | logger.debug('\n\nRunning run_re_version_vars...') 71 | query = version() 72 | # query._args.first = 3 73 | # query._args.after = 'Mg==' 74 | query.name = 'myVersionQuery' 75 | 76 | query._args_type = ArgType.VARIABLES 77 | 78 | try: 79 | logger.debug('gqlSource GQL version: ' + query.export_gql_source) 80 | 81 | response = requests.request('POST', url=RE_URL, 82 | json={ "query": query.export_gql_source, "variables": query.export_gqlvariables }, 83 | headers=RE_HEADERS) 84 | gqlResponse = GQLResponse(response) 85 | 86 | gqlResponse.print_msg_out() 87 | gqlResponse.map_gqldata_to_obj(query.type) 88 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 89 | except Exception as ex: 90 | raise ex 91 | 92 | logger.debug("End of run_re_version_vars") 93 | 94 | def run_re_allcoops(): 95 | logger.debug('\n\nRunning test_gdbc_complex_obj...') 96 | ##STEP 1 97 | query = allCoops() 98 | ## 99 | 100 | insightsChartDataInput = JFUJX_InsightsChartDataOutput_Field.InsightsChartDataOutputArgs() 101 | insightsChartDataInput.input = InsightsChartDataInput() 102 | insightsChartDataInput.input.startDate = '2022-12-12' 103 | insightsChartDataInput.input.endDate = '2023-12-12' 104 | insightsChartDataInput.input.genFarmId = "02305" 105 | insightsChartDataInput.input.period = Period.Year 106 | 107 | query.type.generationfarm.insightsChartData._args = insightsChartDataInput 108 | ##RESULT a) and b) 109 | print('Query GQL syntax: ' + query.export_gql_source) 110 | ## 111 | 112 | ##STEP 4 113 | try: 114 | response = requests.request('POST', url=RE_URL, 115 | json={ "query": query.export_gql_source }, 116 | headers=RE_HEADERS) 117 | ## 118 | 119 | ##STEP 5 120 | from pygqlmap.network import GQLResponse 121 | 122 | gqlResponse = GQLResponse(response) 123 | ## 124 | 125 | gqlResponse.print_msg_out() 126 | 127 | ##STEP 6 128 | gqlResponse.map_gqldata_to_obj(query.type) 129 | ## 130 | 131 | ##RESULT c) 132 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 133 | ## 134 | 135 | except Exception as ex: 136 | raise ex 137 | 138 | logger.debug("End of test_gdbc_complex_obj") 139 | -------------------------------------------------------------------------------- /tests/README.MD: -------------------------------------------------------------------------------- 1 | # py-graphql-mapper - Test 2 | 3 | Tests have been made using: 4 | 5 | - GeoDB Cities GraphQL API 6 | - Github GraphQL API 7 | - rapidapi GraphQL API 8 | 9 | 10 | In order to launch the tests locally an API key has to be set for: 11 | 12 | * GeoDBCities API [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/consts.py), available with a free subscription at https://rapidapi.com/wirefreethought/api/geodb-cities-graphql/ 13 | * Github API [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/consts.py), available with a free subscription at https://github.com 14 | * RapidAPI API [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/consts.py), available with a free subscription at https://rapidapi.com/ApiPlaygroundTestRapidapi/api/graphql-rapidapi-test/ 15 | 16 | 17 | ## Queries 18 | 19 | | Scenario | Description | Test case | 20 | |:---------|:-----------|:----------:| 21 | | Querying a simple object type | Create a GraphQL Query to fetch a simple GraphQL type and build the python class instance containing the data from the response
_Note: This is only for descriptive purposes, the GraphQL server involved does not expose such a query, for real-world cases see other tests_ | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/simple_obj_test.py)| 22 | | Querying a simple object type using arguments as literal values | Create a GraphQL Query to fetch a simple GraphQL type and build the python class instance containing the data from the response
_Note: This is only for descriptive purposes, the GraphQL server involved does not expose such a query, for real-world cases see other tests_ | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/simple_obj_args_literal_test.py) | 23 | | Querying a simple object type using arguments and variables | Create a GraphQL Query to fetch a simple GraphQL type and build the python class instance containing the data from the response
_Note: This is only for descriptive purposes, the GraphQL server involved does not expose such a query, for real-world cases see other tests_ | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/simple_obj_args_vars_test.py) | 24 | | Querying a simple object type changing the visibility of fields | Create a GraphQL Query to fetch a simple GraphQL type and build the python class instance containing the data from the response
_Note: This is only for descriptive purposes, the GraphQL server involved does not expose such a query, for real-world cases see other tests_ | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/simple_obj_viewchange_test.py) | 25 | | Querying a connection | Create a GraphQL Query to fetch a GraphQL connection type and build the python class instance containing the data from the response | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/connobj_test.py) | 26 | | Querying a connection using arguments as literal values | Create a GraphQL Query to fetch a GraphQL connection type using Args as literal values and build the python class instance containing the data from the response | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/connobj_args_literal_test.py) | 27 | | Querying a connection using arguments and variables | Create a GraphQL Query to fetch a GraphQL connection type using Args and Variables and build the python class instance containing the data from the response | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/connobj_args_vars_test.py) | 28 | | Querying a connection changing the visibility of fields | Create a GraphQL Query to fetch a simple GraphQL type and build the python class instance containing the data from the response
_Note: This is only for descriptive purposes, the GraphQL server involved does not expose such a query, for real-world cases see other tests_ | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/connobj_viewchange_test.py) | 29 | | Querying a composed object | Create a GraphQL Query to fetch a GraphQL connection type using Args and Variables and build the python class instance containing the data from the response | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstquery/complex_obj_test.py) | 30 | 31 | Further queries can be found [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/unittests.py) 32 | 33 | ## Mutations 34 | 35 | | Scenario | Description | Test case | 36 | |:---------|:-----------:|----------:| 37 | | Executing a mutation with arguments as literal values | create a GraphQL Mutation to modify a GraphQL type and builds the python class instance containing the payload received from the response | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstmutation/mutation_test.py) | 38 | | Executing a mutation manually created using only _pygqlmap_ with arguments as literal values | create a GraphQL Mutation to mutate a GraphQL type and builds the python class instance containing the payload received from the response | [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/tstmutation/manual_mutation_test.py) | 39 | 40 | 41 | Further tests can be found [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/unittests.py) 42 | -------------------------------------------------------------------------------- /codegen/src/base_class.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import logging as logger 3 | from pygqlmap.src.consts import GQLLIST_PREFIX, NON_NULL_PREFIX, STRING_GQLLIST_BUILTIN 4 | from pygqlmap.src.translator import Translate 5 | from .enums import TypeKind 6 | from .utils import gqlTypeKinds, typesByName 7 | 8 | class SchemaTypeManager(): 9 | 10 | def compose_py_type(self, is_arg: bool = False) -> str: 11 | is_list = False 12 | type_out = '' 13 | is_nonnull = False 14 | 15 | if not self.type_defs: 16 | logger.error('Need to extract type definition first') 17 | for type_def in self.type_defs: 18 | try: 19 | py_type = Translate.to_python_type(type_def) 20 | if py_type == TypeKind.NON_NULL.name: 21 | is_nonnull = True 22 | continue 23 | elif py_type == TypeKind.LIST.name: 24 | is_list = True 25 | type_out += (NON_NULL_PREFIX if is_nonnull and is_arg else '') + GQLLIST_PREFIX 26 | if is_nonnull: is_nonnull = False 27 | continue 28 | 29 | if is_list: 30 | #check if py type declaration is explicitly a builtin or if ENUM or SCALAR in gql types included 31 | if py_type in STRING_GQLLIST_BUILTIN or 'ENUM' in self.get_used_typekinds() or 'SCALAR' in self.get_used_typekinds(): 32 | type_out = type_out.removesuffix('_') 33 | type_out += '[' 34 | else: 35 | type_out += py_type + '[' 36 | if is_nonnull and is_arg: ##if it is not an argument we can ignore the nonnullability, it is responsibility of the server 37 | type_out += NON_NULL_PREFIX 38 | 39 | type_out += py_type 40 | 41 | except Exception as ex: 42 | raise Exception('Error during composition of type ' + type_def + ' for ' + self.name + ' - ' + ex.args[0]) 43 | 44 | if is_list: type_out += ']' 45 | 46 | self.type_defs.clear() 47 | return type_out, py_type 48 | 49 | def get_used_typekinds(self, gql_typekind_list_param:list = None): 50 | try: 51 | if gql_typekind_list_param == None: gql_typekind_list_param = [] 52 | gql_typekind_list = copy.deepcopy(gql_typekind_list_param) 53 | if hasattr(self, 'kind') and self.kind: 54 | gql_typekind_list.append(self.kind) 55 | if hasattr(self, 'ofType') and self.ofType: 56 | return self.ofType.get_used_typekinds(gql_typekind_list) 57 | if hasattr(self, 'type') and self.type: 58 | return self.type.get_used_typekinds(gql_typekind_list) 59 | except Exception as ex: 60 | raise Exception('Error during etraction of used object GraphQL names' + ' - ' + ex.args[0]) 61 | 62 | return gql_typekind_list 63 | 64 | def get_used_typenames(self, gql_typename_list_param:list = None): 65 | try: 66 | if gql_typename_list_param == None: gql_typename_list_param = [] 67 | gql_typename_list = copy.deepcopy(gql_typename_list_param) 68 | if hasattr(self, 'kind') and self.kind and self.kind in gqlTypeKinds: 69 | gql_typename_list.append(self.name) 70 | if hasattr(self, 'ofType') and self.ofType: 71 | return self.ofType.get_used_typenames(gql_typename_list) 72 | if hasattr(self, 'type') and self.type: 73 | return self.type.get_used_typenames(gql_typename_list) 74 | except Exception as ex: 75 | raise Exception('Error during etraction of used object GraphQL names' + ' - ' + ex.args[0]) 76 | 77 | return gql_typename_list 78 | 79 | def get_objtype_defs(self, typeDefListParam = None): 80 | if typeDefListParam == None: typeDefListParam = [] 81 | typeDefList = copy.deepcopy(typeDefListParam) 82 | 83 | try: 84 | if hasattr(self, 'kind') and self.kind: 85 | if self.kind in typesByName: 86 | typeDefList.append(self.name) 87 | else: 88 | typeDefList.append(self.kind) 89 | if hasattr(self, 'ofType') and self.ofType: 90 | return self.ofType.get_objtype_defs(typeDefList) 91 | if hasattr(self, 'type') and self.type: 92 | return self.type.get_objtype_defs(typeDefList) 93 | except Exception as ex: 94 | raise Exception('Error during etraction of object type definitions' + ' - ' + ex.args[0]) 95 | 96 | return typeDefList 97 | 98 | def get_valid_fields_lst(self): 99 | try: 100 | if hasattr(self, 'args') and self.args: ##Schema Field 101 | return self.args 102 | elif hasattr(self, 'fields') and self.fields: ##OBJECT, INTERFACE 103 | return self.fields 104 | elif hasattr(self, 'inputFields') and self.inputFields: ##INPUT_OBJECT 105 | return self.inputFields 106 | elif hasattr(self, 'possibleTypes') and self.possibleTypes: ##UNION 107 | return self.possibleTypes.values() 108 | else: ##No list of fields 109 | return [] 110 | except Exception as ex: 111 | raise Exception('Error getting fields list for type ' + self.name + ' - ' + ex.args[0]) 112 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | [![Python package](https://github.com/dapalex/py-graphql-mapper/actions/workflows/python-package.yml/badge.svg)](https://github.com/dapalex/py-graphql-mapper/actions/workflows/python-package.yml) 2 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/280533e425784f7da9ecb0f6e529886b)](https://www.codacy.com/gh/dapalex/py-graphql-mapper/dashboard?utm_source=github.com&utm_medium=referral&utm_content=dapalex/py-graphql-mapper&utm_campaign=Badge_Grade) 3 | -------------------------------------------------------------------------------- 4 | # py-graphql-mapper 5 | [![Code Generation Test](https://github.com/dapalex/py-graphql-mapper/actions/workflows/test-codegen.yml/badge.svg)](https://github.com/dapalex/py-graphql-mapper/actions/workflows/test-codegen.yml) 6 | [![Pyhon-GraphQL Mapping Test](https://github.com/dapalex/py-graphql-mapper/actions/workflows/test-map.yml/badge.svg)](https://github.com/dapalex/py-graphql-mapper/actions/workflows/test-map.yml) 7 | 8 | A python library to interact with GraphQL APIs with no need of hardcoded strings. 9 | 10 | ## Introduction 11 | 12 | This library acts as a mapper between python and GraphQL languages for GraphQL clients, allowing a code-first approach when calling a GraphQL API server. 13 | It translates GraphQL entities into python objects and viceversa in order to avoid working with massive "copy&paste"s. 14 | 15 | This document contains a quick overview of the functionalities, for more details and options you can read here: 16 | 17 | * [Code Generation](https://github.com/dapalex/py-graphql-mapper/blob/develop/codegen/README.MD) 18 | * [Core Mapper](https://github.com/dapalex/py-graphql-mapper/blob/develop/pygqlmap/README.MD) 19 | * [Use Cases](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/README.MD) 20 | 21 | 22 | The package does not use any third-party libraries, it relies only on python 3 (3.10+) standard libraries. 23 | 24 | 25 | ## Usage in a nutshell 26 | 27 | ### Installation 28 | 29 | Available in PyPI, the following command will install the library: 30 | 31 | ``` 32 | pip install py-graphql-mapper 33 | ``` 34 | 35 | 36 | ### Generate python code from schema 37 | 38 | To generate the code execute the following command: 39 | 40 | ``` 41 | pgmcodegen generate ./pathToOutputFolder -apiArgs .//generatorArgs.json 42 | ``` 43 | 44 | This command requires a json file containing the parameters needed to get the GraphQL schema 45 | 46 | ![image](https://github.com/dapalex/py-graphql-mapper/blob/develop/docs/cli_args_nutshell.png) 47 | 48 | A sample is available in the main folder ['cli_args.json'](https://github.com/dapalex/py-graphql-mapper/blob/develop/cli_args.json). 49 | 50 | The following python files will be generated: 51 | 52 | * enums.py 53 | * scalars.py 54 | * gql_simple_types.py 55 | * gql_types.py 56 | * type_refs.py 57 | * queries.py 58 | * mutations.py 59 | 60 | These links show code generated using the library [Github GraphQL API](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/output/github), [Rapid GraphQL API](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/output/rapidapi) and [GeoDBCities API](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/output/gdbc) 61 | 62 | More command options are available [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/codegen/README.MD#usage-via-command-line) 63 | 64 | 65 | ### Execution of a query 66 | 67 | Choose the query class you want to use from the generated file queries.py (or a mutation from mutations.py): 68 | 69 | Instantiate it adding GraphQL arguments if needed: 70 | ```python 71 | from .output.gdbc.queries import currencies 72 | 73 | my_currencies = currencies(last=7, before='MTE=') 74 | ``` 75 | or add them using the field _args_ 76 | 77 | ```python 78 | my_currencies._args.last = 7 79 | my_currencies._args.before = 'MTE=' 80 | ``` 81 | Then call _export_gql_source_ property to pass the payload to the HTTP request: 82 | 83 | (example using _requests_ library) 84 | ```python 85 | import requests 86 | 87 | response = requests.request('POST', url='https://geodb-cities-graphql.p.rapidapi.com/', 88 | json= { "query": my_currencies.export_gql_source }, 89 | headers={ 90 | "content-type": "application/json", 91 | "X-RapidAPI-Key": '123402mmri02fni230iif32jr420', 92 | "X-RapidAPI-Host": "geodb-cities-graphql.p.rapidapi.com" 93 | } 94 | ) 95 | ``` 96 | 97 | More details on how to set a query [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/pygqlmap/README.MD#creation-of-an-operation) 98 | 99 | 100 | ### Retrieval of a response 101 | 102 | Obtained the response from the GraphQL API the following code will map the received json payload into the python object 103 | 104 | ```python 105 | from pygqlmap.network import GQLResponse 106 | 107 | gqlResponse = GQLResponse(response) 108 | 109 | gqlResponse.map_gqldata_to_obj(myCurrenciesQuery.type) 110 | 111 | print('Result object: ' + str(gqlResponse.result_obj)) 112 | ``` 113 | 114 | The mapped response from the GraphQL server will be available within _gqlResponse_ object: `_gqlResponse.result_obj_` 115 | 116 | More details [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/pygqlmap/README.MD#parsing-of-a-response) 117 | 118 | 119 | A suite of use cases [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/README.MD) 120 | 121 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://github.com/sponsors/dapalex?frequency=one-time&sponsor=dapalex) 122 | -------------------------------------------------------------------------------- /tests/tstmutation/manual_mutation_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | USE CASE DESCRIPTION: 4 | This test shows how to create a GraphQL Mutation using arguments as Literal Values to CREATE a GraphQL record and build the python class instance containing the data from the response 5 | 6 | Mutation to reproduce: 7 | 8 | mutation myCreateDiscussion { 9 | createDiscussion(input: { repositoryId: "R_kgDOH7MI4g", title: "My Title", body: "Some text to give info", categoryId: 1, clientMutationId: "Client1" } ){ 10 | clientMutationId 11 | discussion { 12 | answerChosenAt 13 | body 14 | bodyHTML 15 | bodyText 16 | createdAt 17 | createdViaEmail 18 | databaseId 19 | id 20 | includesCreatedEdit 21 | lastEditedAt 22 | locked 23 | number 24 | publishedAt 25 | repository { 26 | allowUpdateBranch 27 | autoMergeAllowed 28 | } 29 | resourcePath 30 | title 31 | updatedAt 32 | upvoteCount 33 | url 34 | viewerCanDelete 35 | viewerCanReact 36 | viewerCanSubscribe 37 | viewerCanUpdate 38 | viewerCanUpvote 39 | viewerDidAuthor 40 | viewerHasUpvoted 41 | } 42 | } 43 | } 44 | 45 | STEP 1: Define the python class corresponding to the GraphQL connection type and 'currency' corresponding the the connection node within the query 46 | """ 47 | ##STEP 1 48 | from pygqlmap.components import GQLArgsSet, GQLObject 49 | from pygqlmap.gql_operations import GQLMutation 50 | from pygqlmap.gql_types import ID 51 | import logging as logger 52 | import requests 53 | from ..utils import stringifyresult 54 | 55 | class Repository(GQLObject): 56 | allowUpdateBranch: bool 57 | autoMergeAllowed: bool ##NON NULL 58 | 59 | class Discussion(GQLObject): 60 | answerChosenAt: str 61 | body: str ##NON NULL 62 | bodyHTML: str ##NON NULL 63 | bodyText: str ##NON NULL 64 | createdAt: str ##NON NULL 65 | createdViaEmail: bool ##NON NULL 66 | databaseId: int 67 | id: ID ##NON NULL 68 | includesCreatedEdit: bool ##NON NULL 69 | lastEditedAt: str 70 | locked: bool ##NON NULL 71 | number: int ##NON NULL 72 | publishedAt: str 73 | repository: Repository ##NON NULL 74 | resourcePath: str ##NON NULL 75 | title: str ##NON NULL 76 | updatedAt: str ##NON NULL 77 | upvoteCount: int ##NON NULL 78 | url: str ##NON NULL 79 | viewerCanDelete: bool ##NON NULL 80 | viewerCanReact: bool ##NON NULL 81 | viewerCanSubscribe: bool ##NON NULL 82 | viewerCanUpdate: bool ##NON NULL 83 | viewerCanUpvote: bool ##NON NULL 84 | viewerDidAuthor: bool ##NON NULL 85 | viewerHasUpvoted: bool ##NON NULL 86 | 87 | class CreateDiscussionInput(GQLObject): 88 | repositoryId: ID ##NON NULL 89 | title: str ##NON NULL 90 | body: str ##NON NULL 91 | categoryId: ID ##NON NULL 92 | clientMutationId: str 93 | 94 | class CreateDiscussionContent(GQLObject): 95 | clientMutationId: str 96 | discussion: Discussion 97 | 98 | class createDiscussion(GQLMutation): 99 | class CreateDiscussionArguments(GQLArgsSet, GQLObject): 100 | input: CreateDiscussionInput ##NON NULL 101 | 102 | _args: CreateDiscussionArguments 103 | 104 | type: CreateDiscussionContent 105 | ## 106 | 107 | """ 108 | STEP 2: Instantiate GQLOperation class representing the GraphQL mutation 109 | STEP 3: Instantiate GQLArgs object structure with argument type as LITERAL_VALUES 110 | STEP 4: Query the GraphQL server 111 | STEP 5: Pass the response received to the GQLResponse constructor 112 | STEP 6: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 113 | 114 | RESULT: The object within gqlResponse.result_obj will contain the data from the GraphQL server 115 | """ 116 | from ..consts import GITHUB_URL, GITHUB_HEADERS 117 | 118 | def run_gh_insert_mutation_literal(): 119 | logger.debug('\n\nRunning run_gh_insert_mutation_literal...') 120 | ##STEP 2 121 | mutation = createDiscussion() 122 | mutation.name = 'myCreateDiscussion' 123 | ## 124 | 125 | ##STEP 3 126 | from pygqlmap.enums import ArgType 127 | 128 | mutation._args_type = ArgType.LITERAL_VALUES 129 | mutation._args.input = CreateDiscussionInput() 130 | mutation._args.input.repositoryId = 'R_kgDOIOMoaA' 131 | mutation._args.input.title = 'My Title' 132 | mutation._args.input.body = 'Some text to give info' 133 | mutation._args.input.categoryId = 'DIC_kwDOIOMoaM4CR_eD' 134 | mutation._args.input.clientMutationId = 'Client1' 135 | 136 | try: 137 | logger.debug('Query GQL syntax: ' + mutation.export_gql_source) 138 | ## 139 | 140 | ##STEP 4 141 | response = requests.request('POST', url=GITHUB_URL, 142 | json={ "query": mutation.export_gql_source }, 143 | headers=GITHUB_HEADERS) 144 | ## 145 | 146 | ##STEP 5 147 | from pygqlmap.network import GQLResponse 148 | 149 | gqlResponse = GQLResponse(response) 150 | ## 151 | 152 | gqlResponse.print_msg_out() 153 | 154 | ##STEP 6 155 | gqlResponse.map_gqldata_to_obj(mutation.type) 156 | ## 157 | 158 | ##RESULT 159 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 160 | ## 161 | except Exception as ex: 162 | raise ex 163 | 164 | logger.debug("End of run_gh_insert_mutation_literal") 165 | -------------------------------------------------------------------------------- /pygqlmap/src/builder.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import logging as logger 3 | from pygqlmap.src.base import Builder 4 | from .enums import BuildingType 5 | from .consts import GQL_BUILTIN 6 | import inspect 7 | 8 | class QueryBuilder(Builder): 9 | """ for internal use only """ 10 | build_sctype: BuildingType 11 | log_progress: bool 12 | 13 | def __init__(self, build_sctype: BuildingType, log_progress: bool = False): 14 | self.build_sctype = build_sctype 15 | self.log_progress = log_progress 16 | 17 | super().__init__() 18 | 19 | def set_py_fields(self, dataInput, opObject, customObject=None): 20 | """ for internal use only """ 21 | newObjModelDict = opObject.__dataclass_fields__ ##maybe asdict better 22 | attr_to_del = [] 23 | 24 | if type(dataInput) == list: 25 | ##get element type 26 | self.set_py_list_fields(dataInput, opObject, customObject, attr_to_del) 27 | else: 28 | self.set_py_fields_inner(newObjModelDict, dataInput, opObject, customObject, attr_to_del) 29 | 30 | # for a in attr_to_del: 31 | # if a in opObject.__dict__.keys(): 32 | # if self.build_sctype == BuildingType.STANDARD: 33 | # attr = getattr(opObject, a) 34 | # del attr 35 | # elif self.build_sctype == BuildingType.ALTERCLASS: 36 | # logger.info('delete attribute from class') 37 | # else: 38 | # logger.info('should do nothing in new object') 39 | 40 | def set_py_list_fields(self, dataInput, opObject, customObject, attr_to_del): 41 | for t in inspect.getmro(type(opObject)): 42 | if t == type(opObject): continue 43 | if t == list: continue 44 | eltype = t 45 | break 46 | 47 | for dataEl in dataInput: 48 | if not dataEl: continue 49 | element = eltype() 50 | self.set_py_fields_inner(element.__dataclass_fields__, dataEl, element, customObject, attr_to_del) 51 | opObject.append(element) 52 | 53 | def set_py_fields_inner(self, newObjModelDict, dataInput, opObject, customObject, attr_to_del): 54 | for el in newObjModelDict: 55 | try: 56 | if self.log_progress: logger.info('Started building of field: ' + el) 57 | 58 | if (not el in opObject.fieldsshow.keys() or not opObject.fieldsshow[el]): 59 | if self.log_progress: logger.warning('Field ' + el + ' in ' + opObject + 'not present in fieldsshow') 60 | continue 61 | 62 | if (hasattr(dataInput, el) or el in dataInput.keys()): 63 | attribute = getattr(opObject, el) 64 | attrType = type(attribute) 65 | if attrType in GQL_BUILTIN or attrType == list: 66 | self.set_py_field_value(opObject, el, dataInput[el]) 67 | elif isinstance(attribute, Enum): 68 | self.set_py_field_value(opObject, el, attrType(dataInput[el])) 69 | else: 70 | if attribute == None: 71 | attribute = type(customObject)() 72 | if dataInput[el]: 73 | if isinstance(dataInput[el], list): 74 | setattr(opObject, el, []) 75 | listObjectElement = getattr(opObject, el) 76 | for subEl in dataInput[el]: 77 | subElObject = attrType() 78 | self.set_py_fields(subEl, subElObject) 79 | listObjectElement.append(subElObject) 80 | else: 81 | setattr(opObject, el, self.build({ el: dataInput[el] }, attribute)) #, newObject 82 | else: 83 | self.set_py_field_value(opObject, el, dataInput[el]) 84 | else: 85 | self.clean_py_value(opObject, el, attr_to_del) 86 | 87 | except Exception as ex: 88 | logger.error('Setting value for element ' + el + ' failed - ' + ex.args[0]) 89 | 90 | def set_py_field_value(self, obj, attr, value): 91 | """ for internal use only """ 92 | if self.log_progress: logger.info('Setting value of: ' + attr) 93 | try: 94 | if obj.fieldsshow: 95 | if obj.fieldsshow[attr]: 96 | if self.build_sctype == BuildingType.STANDARD or self.build_sctype == BuildingType.ALTERCLASS: 97 | setattr(obj, attr, value) 98 | else: 99 | logger.info('new object management') 100 | except Exception as ex: 101 | logger.error('Setting value of: ' + attr + ' failed - ' + ex.args[0]) 102 | 103 | def clean_py_value(self, obj, field, attr_to_del): 104 | """ for internal use only """ 105 | if self.log_progress: logger.info('Cleaning value of: ' + field) 106 | if self.build_sctype == BuildingType.STANDARD: 107 | setattr(obj, field, None) 108 | elif self.build_sctype == BuildingType.ALTERCLASS: 109 | logger.info('alter class to delete field') 110 | attr_to_del.append(field) 111 | elif self.build_sctype == BuildingType.CREATENEWCLASS: 112 | logger.info('do nothing in new class') 113 | else: 114 | raise Exception('build_sctype not assigned') 115 | 116 | class ResultBuilder(QueryBuilder): 117 | def set_py_list_fields(self, dataInput, opObject, customObject, attr_to_del): 118 | for t in inspect.getmro(type(opObject)): 119 | if t == type(opObject): continue 120 | if t == list: continue 121 | eltype = t 122 | break 123 | 124 | for dataEl in dataInput: 125 | if not dataEl: continue 126 | element = eltype() 127 | self.set_py_fields_inner(element.__dataclass_fields__, dataEl, element, customObject, attr_to_del) 128 | opObject.append(element) 129 | 130 | for d_field in opObject.__dataclass_fields__: 131 | delattr(opObject, d_field) 132 | return 133 | -------------------------------------------------------------------------------- /tests/spot_test.py: -------------------------------------------------------------------------------- 1 | 2 | import abc 3 | from datetime import datetime 4 | import sys 5 | import os 6 | import pathlib 7 | import logging as logger 8 | import requests 9 | 10 | 11 | 12 | 13 | sys.path.append(str(pathlib.Path(os.path.dirname(sys.path[0])).absolute())) 14 | 15 | from pygqlmap.gql_operations import GQLQuery 16 | 17 | from tests.output.re.enums import Period 18 | from tests.output.re_nodesc.gql_simple_types import InsightsChartDataInput 19 | from tests.output.re.gql_types import IROOZ_InsightsChartDataOutput_Field 20 | from tests.output.re_nodesc.gql_types import InsightsChartDataOutput 21 | from tests.output.re_nodesc.queries import allCoops, version 22 | from tests.utils import stringifyresult 23 | from tests.output.github.gql_types import AddCommentInput 24 | # from output.github.queries import rateLimit 25 | from codegen.generator import CodeGenerator 26 | # from datetime import datetime 27 | from codegen.network import fetch_schema_obj 28 | from codegen.query_presets import QUERY_SCHEMA_AND_TYPES 29 | from pygqlmap.enums import ArgType 30 | from pygqlmap.gql_types import ID, NonNull_ID 31 | from pygqlmap.network import GQLResponse 32 | from consts import GDBC_HEADERS, GDBC_URL 33 | # from output.gdbc.gql_simple_types import Currency 34 | # from output.gdbc.queries import currencies 35 | from consts import * 36 | # from output.github.gql_types import AddCommentInput 37 | # from output.gdbc.queries import country 38 | # from output.gdbc.gql_types import DisplayOptions 39 | # from output.rapidapi.gql_simple_types import JYWXC_name_Field 40 | 41 | """ 42 | USE CASE DESCRIPTION: 43 | This test shows how to create a GraphQL Query to fetch a GraphQL complex type (made of objects, connections, and scalar fields) combining arguments usage as literal values 44 | with visibility usage for chosen fields, and finally build the python class instance containing the data from the response 45 | 46 | Query to reproduce: 47 | 48 | query myCountriesQuery { 49 | countries { 50 | edges { 51 | cursor 52 | node { 53 | code 54 | callingCode (*) 55 | wikiDataId 56 | capital 57 | name 58 | currencyCodes (*) 59 | flagImageUri 60 | numRegions 61 | region { 62 | fipsCode (*) 63 | isoCode (*) 64 | wikiDataId (*) 65 | name 66 | capital (*) 67 | country { (*) 68 | code 69 | callingCode 70 | wikiDataId 71 | capital 72 | name 73 | currencyCodes 74 | flagImageUri 75 | numRegions 76 | } 77 | populatedPlaces { (*) 78 | totalCount 79 | } 80 | } 81 | regions { 82 | edges { 83 | cursor 84 | } 85 | totalCount 86 | pageInfo { 87 | startCursor 88 | endCursor 89 | hasNextPage (*) 90 | hasPreviousPage 91 | } 92 | } 93 | } 94 | } 95 | totalCount 96 | pageInfo { 97 | startCursor 98 | endCursor 99 | hasNextPage (*) 100 | hasPreviousPage 101 | } 102 | } 103 | } 104 | 105 | 106 | STEP 1: Instantiate the class representing the GraphQL query 107 | STEP 2: Set the arguments 108 | STEP 3: Call set_show function of GQLOperation class to set the visibility of chosen fields (path to declare with dot notation) 109 | STEP 4: Query the GraphQL server 110 | STEP 5: Pass the response received to the GQLResponse constructor 111 | STEP 6: Call map_gqldata_to_obj() function to obtain the python class with data from GraphQL server 112 | 113 | RESULT: 114 | a) The request toward the GraphQL server will not have the hidden fields 115 | b) The request toward the GraphQL server will have the query with arguments 'currencyCode' and 'code' 116 | b) The python class instance obtained from the response will not have the hidden fields 117 | """ 118 | 119 | import requests 120 | from consts import GDBC_HEADERS, GDBC_URL 121 | # from ..utils import ManageException 122 | from output.gdbc.queries import countries 123 | import logging as logger 124 | from utils import stringifyresult 125 | 126 | def run_allcoops_findthisinterfaceunionorwhat(): 127 | logger.debug('\n\nRunning test_gdbc_complex_obj...') 128 | ##STEP 1 129 | query = allCoops() 130 | ## 131 | 132 | insightsChartDataInput = IROOZ_InsightsChartDataOutput_Field.InsightsChartDataOutputArgs() 133 | insightsChartDataInput.input = InsightsChartDataInput() 134 | insightsChartDataInput.input.startDate = '2022-12-12' 135 | insightsChartDataInput.input.endDate = '2023-12-12' 136 | insightsChartDataInput.input.genFarmId = "02305" 137 | insightsChartDataInput.input.period = Period.Year 138 | 139 | query.type.generationfarm.insightsChartData._args = insightsChartDataInput 140 | ##RESULT a) and b) 141 | print('Query GQL syntax: ' + query.export_gql_source) 142 | ## 143 | 144 | ##STEP 4 145 | try: 146 | response = requests.request('POST', url=RE_URL, 147 | json={ "query": query.export_gql_source }, 148 | headers=RE_HEADERS) 149 | ## 150 | 151 | ##STEP 5 152 | from pygqlmap.network import GQLResponse 153 | 154 | gqlResponse = GQLResponse(response) 155 | ## 156 | 157 | gqlResponse.print_msg_out() 158 | 159 | ##STEP 6 160 | gqlResponse.map_gqldata_to_obj(query.type) 161 | ## 162 | 163 | ##RESULT c) 164 | logger.info('result object: ' + stringifyresult(gqlResponse.result_obj)) 165 | ## 166 | 167 | except Exception as ex: 168 | raise ex 169 | 170 | logger.debug("End of test_gdbc_complex_obj") 171 | 172 | run_allcoops_findthisinterfaceunionorwhat() -------------------------------------------------------------------------------- /codegen/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import pathlib 5 | import sys 6 | # import logging as logger 7 | from .query_presets import * 8 | from .network import fetch_schema_response 9 | from .generator import CodeGenerator, build_schema 10 | from .src.sp_schema import GQLSchema 11 | from .src.utils import get_valid_folder 12 | 13 | def main(): 14 | try: 15 | 16 | my_parser = argparse.ArgumentParser(description = """Welcome to the python-graphql mutations generator""") 17 | my_parser.add_argument('command', type=str, help='the requested command') 18 | my_parser.add_argument('destPath', type=str, help='the destination path of mutation classes') 19 | 20 | args = my_parser.parse_args(sys.argv[1:3]) 21 | 22 | commandParser = argparse.ArgumentParser() 23 | commandParser.add_argument('-v', '--verbose', action='store_true') 24 | commandParser.add_argument('-apiArgs', type=str, help='path of the args file', required=False) 25 | 26 | if args.command == 'generate': 27 | commandParser.add_argument('-schemaFile', type=str, help='path of the args file', required=False) 28 | 29 | commandArgs = commandParser.parse_args(sys.argv[3:]) 30 | 31 | if args.command == 'generate': 32 | generate_py_code(commandArgs, args.destPath) 33 | elif args.command == 'download': 34 | save_json_schema(commandArgs, args.destPath) 35 | else: 36 | raise argparse.ArgumentTypeError('Invalid command') 37 | 38 | except Exception as ex: 39 | print(str(ex.args)) 40 | exit(-1) 41 | 42 | def save_json_schema(args, destination): 43 | try: 44 | if hasattr(args, 'apiArgs') and args.apiArgs: 45 | if args.verbose: print('Checking args file...') 46 | argsFilePath = pathlib.Path(args.apiArgs).absolute() 47 | if args.verbose: print('args file path -> ' + str(argsFilePath)) 48 | try: 49 | if args.verbose: print('Reading args file...') 50 | with open(str(argsFilePath), 'r') as argsFile: 51 | arguments = json.load(argsFile) 52 | except Exception as ex: 53 | print('saveJsonSchema arg opening error: ' + str(ex.args)) 54 | exit(-1) 55 | 56 | if args.verbose: print(arguments) 57 | 58 | if 'apiURL' in arguments.keys() and arguments['apiURL']: 59 | httpHeaders = arguments['httpHeaders'] if 'httpHeaders' in arguments.keys() else None 60 | if args.verbose: print('Fetching schema from server...') 61 | schemaResponse = fetch_schema_response(arguments['apiURL'], httpHeaders, QUERY_SCHEMA_AND_TYPES) 62 | 63 | parentFolder = pathlib.Path(destination).parent 64 | outputFolder = get_valid_folder(str(parentFolder)) 65 | fileName = pathlib.Path(destination).name 66 | outputFile = os.path.join(outputFolder, fileName) 67 | if args.verbose: print('Saving Schema...') 68 | with open(outputFile, 'w') as schemaFile: 69 | schemaFile.write(json.dumps(schemaResponse.data, indent=2)) 70 | 71 | except Exception as ex: 72 | print('saveJsonSchema error: ' + str(ex.args)) 73 | exit(-1) 74 | 75 | def generate_py_code(args, destPath): 76 | try: 77 | schemaObject: GQLSchema = None 78 | 79 | if destPath: 80 | if args.verbose: print('Checking destination folder...') 81 | 82 | input_path = get_valid_folder(destPath) 83 | 84 | if args.verbose: print('Parsing command...') 85 | 86 | if hasattr(args, 'apiArgs') and args.apiArgs: 87 | if args.verbose: print('Checking args file...') 88 | argsFilePath = pathlib.Path(args.apiArgs).absolute() 89 | if args.verbose: print('args file path -> ' + str(argsFilePath)) 90 | try: 91 | if args.verbose: print('Reading args file...') 92 | with open(str(argsFilePath), 'r') as argsFile: 93 | arguments = json.load(argsFile) 94 | 95 | if args.verbose: print(arguments) 96 | except Exception as ex: 97 | print('generatePythonCode - arg opening error: ' + str(ex.args)) 98 | exit(-1) 99 | 100 | add_desc = arguments['addDescToGeneratedFiles'] if 'addDescToGeneratedFiles' in arguments.keys() else True 101 | 102 | schemaObject = extract_schema_obj(arguments, args.verbose) 103 | 104 | if args.verbose: print('Generating mutations...') #, end="\r") 105 | CodeGenerator.generate_code(schemaObject, input_path, log_progress=args.verbose, add_desc=add_desc) 106 | except Exception as ex: 107 | print('generatePythonCode error: ' + str(ex.args)) 108 | exit(-1) 109 | 110 | def extract_schema_obj(arguments, verbose): 111 | try: 112 | if 'apiURL' in arguments.keys() and arguments['apiURL']: 113 | httpHeaders = arguments['httpHeaders'] if 'httpHeaders' in arguments.keys() else None 114 | if verbose: print('Fetching schema from server...') 115 | schemaResponse = fetch_schema_response(arguments['apiURL'], httpHeaders, QUERY_SCHEMA_AND_TYPES) 116 | if verbose: print('Mapping response...') 117 | schemaResponse.map_gqldata_to_obj() 118 | 119 | return schemaResponse.result_obj 120 | 121 | elif 'schemaFile' in arguments.keys() and arguments['schemaFile']: 122 | if verbose: print('Extracting schema from file...') 123 | parentFolder = pathlib.Path(arguments['schemaFile']).parent 124 | outputFolder = get_valid_folder(str(parentFolder)) 125 | if verbose: print('output folder: ' + str(outputFolder)) 126 | fileName = pathlib.Path(arguments['schemaFile']).name 127 | if verbose: print('file name: ' + str(fileName)) 128 | outputFilePath = os.path.join(outputFolder, fileName) 129 | if verbose: print('output destination: ' + str(outputFilePath)) 130 | with open(outputFilePath, 'r') as wrapper: 131 | schemaString = wrapper.read() 132 | 133 | if verbose: print('Creating schema object...') 134 | return build_schema(schemaString) 135 | 136 | except Exception as ex: 137 | print('extractSchemaObject error: ' + str(ex.args)) 138 | exit(-1) 139 | 140 | if verbose: print('Schema not extracted!') 141 | return None 142 | 143 | if __name__ == '__main__': 144 | print('python-graphql mapper code generator 0.3.0') 145 | main() -------------------------------------------------------------------------------- /pygqlmap/src/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import asdict 3 | import inspect 4 | import logging as logger 5 | from pygqlmap.helper import handle_recursive_ex 6 | from .consts import COMMA_CONCAT, ARGS_DECLARE 7 | from ..enums import ArgType 8 | 9 | from .utils import is_empty_field 10 | from .translator import Translate 11 | 12 | class FieldsShow(ABC): 13 | 14 | @property 15 | def fieldsshow(self): 16 | return self._fieldsshow 17 | 18 | def init_fieldshow(self): 19 | """ For internal use only """ 20 | try: 21 | self._fieldsshow = asdict(self) 22 | try: 23 | for field in self._fieldsshow: 24 | self._fieldsshow[field] = True 25 | except Exception as ex: 26 | raise handle_recursive_ex(ex, 'Error during fieldShow initialization for field ' + field) 27 | except Exception as ex: 28 | raise handle_recursive_ex(ex, 'Error during fieldShow initialization') 29 | 30 | class Builder(): 31 | @abstractmethod 32 | def build(self, dataInput, pyObject): 33 | pass 34 | 35 | @abstractmethod 36 | def set_py_fields(self, dataInput, opObject, customObject=None): 37 | pass 38 | 39 | def build(self, inputDict: dict, pyObject: any): 40 | """ for internal use only """ 41 | 42 | try: 43 | if self.log_progress: logger.info('Started building of python object: ' + pyObject.__class__.__name__) 44 | item = inputDict.popitem() ##extract the KV pair containing object name and content 45 | 46 | if not item[1] == None: 47 | self.set_py_fields(item[1], pyObject) 48 | else: 49 | pyObject = item[1] 50 | logger.info(item[0] + ' has no content') 51 | 52 | except Exception as ex: 53 | logger.error('Building of python object failed - ' + ex.args[0]) 54 | 55 | return pyObject 56 | 57 | class GQLExporter(): 58 | 59 | log_progress: bool 60 | 61 | @property 62 | def export_gql_source(self): 63 | gqlArgs, outputGqlDict = self.export_gql_dict 64 | return gqlArgs + ' { ' + Translate.graphqlize(outputGqlDict) + ' } ' 65 | 66 | @property 67 | def export_gql_dict(self): 68 | """Return the GraphQL syntax for the current object 69 | 70 | Returns: 71 | str: GraphQL object exported 72 | """ 73 | if hasattr(self, 'log_progress') and self.log_progress: logger.info('Started GQL extraction of python: ' + self.__class__.__name__) 74 | 75 | gqlDict = asdict(self) 76 | outputGqlDict = {} 77 | 78 | from pygqlmap.src.arg_builtin import ArguedBuiltin 79 | try: 80 | for field in gqlDict.keys(): 81 | try: 82 | if field == ARGS_DECLARE: continue 83 | if self._fieldsshow[field]: 84 | fieldObject = getattr(self, field) 85 | if fieldObject == None: continue 86 | if ArguedBuiltin in inspect.getmro(type(fieldObject)): 87 | builtinArgs = fieldObject._args.export_args 88 | outputGqlDict[Translate.to_graphql_field_name(field)] = builtinArgs, Translate.to_graphql_field_name(field) 89 | elif FieldsShow in inspect.getmro(type(fieldObject)): 90 | outputGqlDict[Translate.to_graphql_field_name(field)] = fieldObject.export_gql_dict 91 | elif list in inspect.getmro(type(fieldObject)): 92 | for list_el in fieldObject: 93 | if FieldsShow in inspect.getmro(type(list_el)): 94 | outputGqlDict[Translate.to_graphql_field_name(field)] = list_el.export_gql_dict 95 | else: 96 | pass 97 | else: 98 | if not fieldObject == None: 99 | outputGqlDict[Translate.to_graphql_field_name(field)] = fieldObject 100 | 101 | except Exception as ex: 102 | raise handle_recursive_ex(ex, 'Issue exporting field ' + self.__class__.__name__ + '.' + field) 103 | 104 | except Exception as ex: 105 | raise handle_recursive_ex(ex, 'Issue during export of gql dictionary') 106 | 107 | gqlArgs = '' 108 | #Arguments management START - after check of fields requested 109 | if hasattr(self, ARGS_DECLARE): 110 | try: 111 | if hasattr(self, 'log_progress') and self.log_progress: logger.info('Started GQL extraction of args for: ' + self.__class__.__name__) 112 | gqlArgs = self._args.export_args 113 | except Exception as ex: 114 | raise handle_recursive_ex(ex, 'Issue exporting _args for ' + str(self.__class__.__name__)) 115 | 116 | #Arguments management END 117 | 118 | if len(outputGqlDict) == 0: 119 | return None, None 120 | 121 | return gqlArgs, outputGqlDict 122 | 123 | class GQLBaseArgsSet(): 124 | 125 | @abstractmethod 126 | def export_arg_key(self, field_name, field_value, field_type): 127 | """ For internal use only """ 128 | raise handle_recursive_ex(Exception('exportArg function not implemented'), '') 129 | 130 | @property 131 | def export_args(self): 132 | arguments = '' 133 | try: 134 | if self._args_type == ArgType.LITERAL_VALUES: 135 | arguments = self.export_gqlargs_and_values 136 | if len(arguments) > 0: 137 | return '(' + arguments + ')' 138 | elif self._args_type == ArgType.VARIABLES: 139 | arguments = self.export_gqlarg_keys 140 | if len(arguments) > 0: 141 | return '(' + arguments + ')' 142 | else: 143 | raise handle_recursive_ex(Exception('No valid argType for '), '') 144 | except Exception as ex: 145 | raise handle_recursive_ex(ex, 'Issue during export arguments for ' + self.__class__.__name__) 146 | 147 | return arguments 148 | 149 | @property 150 | def export_gqlarg_keys(self): 151 | """ For internal use only """ 152 | output = '' 153 | try: 154 | for field in self.__dataclass_fields__: 155 | if is_empty_field(getattr(self, field)): continue 156 | try: 157 | output += self.export_arg_key(field, getattr(self, field), self.__dataclass_fields__[field].type) + COMMA_CONCAT 158 | except: 159 | raise handle_recursive_ex(Exception('Issue exporting arg key for: ' + field), '') 160 | 161 | output = output.removesuffix(COMMA_CONCAT) 162 | except Exception as ex: 163 | raise handle_recursive_ex(ex, 'Issue exporting arg keys for ' + str(self.__class__)) 164 | 165 | return output 166 | -------------------------------------------------------------------------------- /tests/output/gdbc_nodesc/gql_types.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, Union, List 2 | from pygqlmap.components import GQLArgsSet, GQLObject 3 | from pygqlmap.gql_types import * 4 | from pygqlmap.src.arg_builtin import * 5 | from typing import NewType 6 | from .gql_simple_types import * 7 | from .enums import * 8 | from .scalars import * 9 | from .type_refs import * 10 | 11 | class Location(GQLObject): 12 | latitude: float 13 | longitude: float 14 | 15 | class DisplayOptions(GQLObject): 16 | asciiMode: bool 17 | language: Language 18 | 19 | class ConnectionPageInfo(GQLObject): 20 | startCursor: str 21 | endCursor: str 22 | hasNextPage: bool 23 | hasPreviousPage: bool 24 | 25 | class TimeZoneEdge(GQLObject): 26 | cursor: str 27 | node: TimeZone 28 | 29 | class list_TimeZoneEdge(list, TimeZoneEdge): pass 30 | 31 | class TimeZonesConnection(GQLObject): 32 | totalCount: int 33 | edges: list_TimeZoneEdge[TimeZoneEdge] 34 | pageInfo: ConnectionPageInfo 35 | 36 | class list_GQLObject(list, GQLObject): pass 37 | 38 | class CountryPopulatedPlacesConnection(GQLObject): 39 | totalCount: int 40 | edges: list_GQLObject[GQLObject] ## Circular Reference for PopulatedPlaceEdge 41 | pageInfo: ConnectionPageInfo 42 | 43 | class RDAZF_RegionPopulatedPlacesConnection_Field(Generic[RegionPopulatedPlacesConnection]): 44 | class RegionPopulatedPlacesConnectionArgs(GQLArgsSet, GQLObject): 45 | namePrefix: str 46 | namePrefixDefaultLangResults: bool 47 | minPopulation: int 48 | maxPopulation: int 49 | timeZoneIds: list[ID] 50 | types: list[str] 51 | sort: str 52 | first: int 53 | after: str 54 | last: int 55 | before: str 56 | includeDeleted: IncludeDeletedFilterType 57 | 58 | _args: RegionPopulatedPlacesConnectionArgs 59 | 60 | 61 | 62 | class CountryRegion(GQLObject): 63 | fipsCode: ID 64 | isoCode: ID 65 | wikiDataId: ID 66 | name: str 67 | capital: str 68 | containingRegion: NewType('CountryRegion', GQLObject) ## Circular Reference for CountryRegion 69 | country: NewType('Country', GQLObject) ## Circular Reference for Country 70 | numPopulatedPlaces: int 71 | populatedPlaces: RDAZF_RegionPopulatedPlacesConnection_Field ## Circular Reference for RegionPopulatedPlacesConnection 72 | 73 | class CountryRegionEdge(GQLObject): 74 | cursor: str 75 | node: CountryRegion 76 | 77 | class list_CountryRegionEdge(list, CountryRegionEdge): pass 78 | 79 | class CountryRegionsConnection(GQLObject): 80 | totalCount: int 81 | edges: list_CountryRegionEdge[CountryRegionEdge] 82 | pageInfo: ConnectionPageInfo 83 | 84 | class LUXDF_CountryPopulatedPlacesConnection_Field(CountryPopulatedPlacesConnection): 85 | class CountryPopulatedPlacesConnectionArgs(GQLArgsSet, GQLObject): 86 | namePrefix: str 87 | namePrefixDefaultLangResults: bool 88 | minPopulation: int 89 | maxPopulation: int 90 | timeZoneIds: list[ID] 91 | types: list[str] 92 | sort: str 93 | first: int 94 | after: str 95 | last: int 96 | before: str 97 | includeDeleted: IncludeDeletedFilterType 98 | 99 | _args: CountryPopulatedPlacesConnectionArgs 100 | 101 | 102 | 103 | class KRLEV_CountryRegion_Field(CountryRegion): 104 | class CountryRegionArgs(GQLArgsSet, GQLObject): 105 | code: ID 106 | 107 | _args: CountryRegionArgs 108 | 109 | 110 | 111 | class QJGAY_CountryRegionsConnection_Field(CountryRegionsConnection): 112 | class CountryRegionsConnectionArgs(GQLArgsSet, GQLObject): 113 | namePrefix: str 114 | namePrefixDefaultLangResults: bool 115 | sort: str 116 | first: int 117 | after: str 118 | last: int 119 | before: str 120 | 121 | _args: CountryRegionsConnectionArgs 122 | 123 | 124 | 125 | class Country(GQLObject): 126 | code: ID 127 | callingCode: str 128 | wikiDataId: ID 129 | capital: str 130 | name: str 131 | currencyCodes: list[str] 132 | flagImageUri: str 133 | numRegions: int 134 | populatedPlaces: LUXDF_CountryPopulatedPlacesConnection_Field 135 | region: KRLEV_CountryRegion_Field 136 | regions: QJGAY_CountryRegionsConnection_Field 137 | 138 | class NearbyPopulatedPlacesConnection(GQLObject): 139 | totalCount: int 140 | edges: list_GQLObject[GQLObject] ## Circular Reference for PopulatedPlaceEdge 141 | pageInfo: ConnectionPageInfo 142 | 143 | class VDGWH_NearbyPopulatedPlacesConnection_Field(NearbyPopulatedPlacesConnection): 144 | class NearbyPopulatedPlacesConnectionArgs(GQLArgsSet, GQLObject): 145 | radius: float 146 | distanceUnit: DistanceUnit 147 | countryIds: list[ID] 148 | excludedCountryIds: list[ID] 149 | namePrefix: str 150 | namePrefixDefaultLangResults: bool 151 | minPopulation: int 152 | maxPopulation: int 153 | timeZoneIds: list[ID] 154 | types: list[str] 155 | sort: str 156 | first: int 157 | after: str 158 | last: int 159 | before: str 160 | includeDeleted: IncludeDeletedFilterType 161 | 162 | _args: NearbyPopulatedPlacesConnectionArgs 163 | 164 | 165 | 166 | class PopulatedPlace(GQLObject): 167 | id: ID 168 | wikiDataId: ID 169 | name: str 170 | placeType: PopulatedPlaceType 171 | elevationMeters: int 172 | latitude: float 173 | longitude: float 174 | population: int 175 | timezone: str 176 | country: Country 177 | region: CountryRegion 178 | distance: PFBPH_distance_Field 179 | locatedIn: NewType('PopulatedPlace', GQLObject) ## Circular Reference for PopulatedPlace 180 | nearbyPopulatedPlaces: VDGWH_NearbyPopulatedPlacesConnection_Field 181 | deleted: bool 182 | 183 | class PopulatedPlaceEdge(GQLObject): 184 | cursor: str 185 | node: PopulatedPlace 186 | 187 | class list_PopulatedPlaceEdge(list, PopulatedPlaceEdge): pass 188 | 189 | class RegionPopulatedPlacesConnection(GQLObject): 190 | totalCount: int 191 | edges: list_PopulatedPlaceEdge[PopulatedPlaceEdge] 192 | pageInfo: ConnectionPageInfo 193 | 194 | class PopulatedPlacesConnection(GQLObject): 195 | totalCount: int 196 | edges: list_PopulatedPlaceEdge[PopulatedPlaceEdge] 197 | pageInfo: ConnectionPageInfo 198 | 199 | class LocaleEdge(GQLObject): 200 | cursor: str 201 | node: Locale 202 | 203 | class list_LocaleEdge(list, LocaleEdge): pass 204 | 205 | class LocalesConnection(GQLObject): 206 | totalCount: int 207 | edges: list_LocaleEdge[LocaleEdge] 208 | pageInfo: ConnectionPageInfo 209 | 210 | class CurrencyEdge(GQLObject): 211 | cursor: str 212 | node: Currency 213 | 214 | class list_CurrencyEdge(list, CurrencyEdge): pass 215 | 216 | class CurrenciesConnection(GQLObject): 217 | totalCount: int 218 | edges: list_CurrencyEdge[CurrencyEdge] 219 | pageInfo: ConnectionPageInfo 220 | 221 | class CountryEdge(GQLObject): 222 | cursor: str 223 | node: Country 224 | 225 | class list_CountryEdge(list, CountryEdge): pass 226 | 227 | class CountriesConnection(GQLObject): 228 | totalCount: int 229 | edges: list_CountryEdge[CountryEdge] 230 | pageInfo: ConnectionPageInfo 231 | -------------------------------------------------------------------------------- /pygqlmap/components.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import inspect 3 | from .helper import handle_recursive_ex 4 | from .src.gql_init import _sub_class_init 5 | from .src.components import FSTree 6 | from .src.base import FieldsShow, GQLExporter, GQLBaseArgsSet 7 | from .src.consts import COMMA_CONCAT, ARGS_DECLARE, PRIMITIVES 8 | from .src.utils import is_empty_field 9 | from .enums import ArgType, OperationType 10 | from .src.translator import Translate 11 | 12 | class GQLOperationArgs(GQLBaseArgsSet): 13 | 14 | @property 15 | def export_gqlarg_keys(self): 16 | 17 | """ For internal use only """ 18 | output = '' 19 | try: 20 | for fieldName, fieldValue in self.arguments.items(): 21 | if is_empty_field(fieldValue[0]): continue 22 | try: 23 | output += self.export_arg_key(fieldName, fieldValue[0], fieldValue[1]) + COMMA_CONCAT 24 | except Exception as ex: 25 | raise handle_recursive_ex(ex, 'Issue exporting arg key for: ' + fieldName) 26 | 27 | output = output.removesuffix(COMMA_CONCAT) 28 | except Exception as ex: 29 | raise handle_recursive_ex(ex, 'Issue exporting arg keys') 30 | 31 | return output 32 | 33 | @property 34 | def export_gqlvariables(self): 35 | """Return the json variables to send to a server 36 | 37 | Returns: 38 | str: json variables exported 39 | """ 40 | if self._args_type == ArgType.VARIABLES: 41 | if self.arguments: 42 | json_fields = dict((key, val[0]) for key, val in self.arguments.items()) 43 | return Translate.to_json_vars(json_fields) 44 | 45 | def export_arg_key(self, field_name, field_value, field_type): 46 | return '$' + Translate.to_graphql_field_name(field_name) + ': ' + Translate.to_graphql_type(field_value, field_type) 47 | 48 | class GQLArgsSet(GQLBaseArgsSet): 49 | 50 | def __init_subclass__(cls): 51 | cls = dataclass(cls) 52 | cls.__init__ = _sub_class_init 53 | 54 | def export_arg_key(self, field_name, field_value, field_type): 55 | """ For internal use only """ 56 | return Translate.to_graphql_field_name(field_name) + ': $' + Translate.to_graphql_field_name(field_name) 57 | 58 | @property 59 | def export_gqlargs_and_values(self): 60 | """ For internal use only """ 61 | return Translate.to_graphql_argsset_definition(self) 62 | 63 | @dataclass 64 | class GQLObject(FieldsShow, GQLExporter): 65 | 66 | def __init_subclass__(cls): 67 | cls = dataclass(cls) 68 | cls.__init__ = _sub_class_init 69 | 70 | class GQLOperation(GQLExporter, GQLOperationArgs): 71 | name: str 72 | obj_type: OperationType 73 | fieldShowTree: FSTree 74 | type: GQLObject 75 | ##key: field name 76 | ##value: (field value, field original type) 77 | arguments: dict = None 78 | 79 | # argumentsRetrieved: False 80 | 81 | def __init__(self, op_type: OperationType, dataType, operationName: str = None, argsType: ArgType = ArgType.LITERAL_VALUES): #, rootName: str = None, inputFieldName: str = None 82 | """_summary_ 83 | 84 | Args: 85 | op_type (OperationType): _description_ 86 | hasArgsAsInput (bool, optional): _description_. Defaults to True. 87 | name (str, optional): _description_. Defaults to 'myQuery'. 88 | log_progress (bool, optional): _description_. Defaults to False. 89 | """ 90 | self.name = operationName if operationName else '' 91 | 92 | self.obj_type = op_type 93 | self.type = dataType 94 | self._args_type = argsType 95 | self.fieldsshowTree = FSTree(self.type, self.__class__.__name__) 96 | self.log_progress = False 97 | 98 | def set_show(self, keys: str or list[str], isVisible: bool): 99 | """_summary_ 100 | 101 | Args: 102 | keys (strorlist[str]): GraphQL field path or list of GraphQL field paths 103 | isVisible (bool): set visibility (default at true) 104 | 105 | Raises: 106 | Exception: keys not string or list of strings 107 | """ 108 | kType = type(keys) 109 | if kType == str: 110 | self.fieldsshowTree.set_fieldshow(keys, isVisible) 111 | elif kType == list: 112 | print('list of keys with same value') 113 | for key in keys: 114 | self.fieldsshowTree.set_fieldshow(key, isVisible) 115 | 116 | @property 117 | def export_gql_source(self): 118 | """Return the GraphQL syntax for the current operation 119 | 120 | Returns: 121 | str: GraphQL Query exported 122 | """ 123 | try: 124 | prefix = self.obj_type.value + ' ' + self.name + ' ' 125 | if hasattr(self.type, 'log_progress'): self.type.log_progress = self.log_progress 126 | 127 | ##Arguments of the operation are arguments of the root object 128 | if hasattr(self, ARGS_DECLARE): 129 | setattr(self.type, ARGS_DECLARE, getattr(self, ARGS_DECLARE)) 130 | 131 | #Update all payload arguments (NESTED ARGUMENTS) with the argument type requested 132 | self.manage_nested_args(self.type) 133 | # self.setArgsLocations(self.type, None, rootName, '') 134 | 135 | if self._args_type == ArgType.VARIABLES: 136 | if hasattr(self, ARGS_DECLARE) and self.arguments: 137 | prefix += '(' + self.export_gqlarg_keys + ')' 138 | if hasattr(self.type, '__dataclass_fields__'): 139 | return prefix + ' { ' + self.__class__.__name__ + self.type.export_gql_source + ' } ' 140 | else: 141 | return prefix + ' { ' + self.__class__.__name__ + ' } ' 142 | except Exception as ex: 143 | raise handle_recursive_ex(ex, 'Issue during export of ' + self.name) 144 | 145 | def manage_nested_args(self, currentObj): 146 | if not self.arguments: self.arguments = {} 147 | try: 148 | if isinstance(currentObj, list): #if it is I have to set the elements as well? 149 | for list_el in currentObj: 150 | self.manage_nested_args(list_el) 151 | 152 | #if obj contains field with arg name, add arg 153 | if type(currentObj) in PRIMITIVES or not hasattr(currentObj, '__dataclass_fields__'): 154 | return 155 | 156 | if hasattr(currentObj, ARGS_DECLARE): 157 | currentObj._args._args_type = self._args_type 158 | for arg in currentObj._args.__dataclass_fields__: 159 | argValue = getattr(currentObj._args, arg) 160 | if is_empty_field(argValue): continue 161 | self.arguments.update({arg: (argValue, currentObj._args.__dataclass_fields__[arg].type)}) 162 | 163 | for subObj in currentObj.__dataclass_fields__: 164 | if subObj == ARGS_DECLARE: continue 165 | self.manage_nested_args(getattr(currentObj, subObj)) 166 | except Exception as ex: 167 | raise handle_recursive_ex(ex, 'Error during args type propagation - ') 168 | -------------------------------------------------------------------------------- /codegen/README.MD: -------------------------------------------------------------------------------- 1 | # py-graphql-mapper - Generator 2 | 3 | ## Introduction 4 | 5 | This module generates automatically python classes corresponding to schema GraphQL types, moreover it can save a GraphQL schema in json format. 6 | The generator can be launched through [command line](#usage-via-command-line) or [programmatically](#usage-programmatically). 7 | 8 | 9 | ## Table of Contents 10 | 11 | 1. [Functionalities](#functionalities) 12 | 1. [Usage programmatically](#usage-programmatically) 13 | 2. [Usage via command line](#usage-via-command-line) 14 | 1. [JSON args file](#json-args-file) 15 | 2. [Using the generated files](#using-the-generated-files) 16 | 17 | 18 | ## Functionalities 19 | 20 | Two functionalities are available: 21 | 22 | 1) Downloading a schema from a GraphQL server API 23 | 2) Generating python classes from a schema 24 | 25 | 26 | ## Usage programmatically 27 | 28 | The class in charge of the generation is _CodeGenerator_, which takes a json schema as input and produces python files containing the mapped GraphQL schema types. 29 | 30 | ### Download of a schema as json file 31 | 32 | If a json version of the schema is not yet available first step is obtaining it, this can be done passing the API Url and HTTP headers to _fetch_schema_obj_ function which will query the GraphQL API server: 33 | 34 | ```python 35 | from codegen.network import fetch_schema_obj 36 | from codegen.queryPresets import QUERY_SCHEMA_AND_TYPES 37 | 38 | schemaObject = fetch_schema_obj(, , QUERY_SCHEMA_AND_TYPES) 39 | ``` 40 | 41 | ### Generation 42 | 43 | If a json schema is already available _build_schema_ can be used: 44 | 45 | ```python 46 | from codegen.generator import build_schema 47 | 48 | schema_obj = build_schema(schema_json_str) 49 | ``` 50 | 51 | Obtained the schema object the following function _CodeGenerator.generate_code_ has to be called: 52 | 53 | ```python 54 | from codegen.generator import CodeGenerator 55 | from codegen.queryPresets import QUERY_SCHEMA_AND_TYPES 56 | 57 | CodeGenerator.generate_code(schema_obj, folder='test\\output\\github\\', log_progress=False, add_desc=True) 58 | ``` 59 | 60 | Required parameters: 61 | 62 | * schema_obj: the schema as python object 63 | * folder: the destination folder where the created python files will be saved 64 | 65 | Optional parameters: 66 | 67 | * log_progress: makes the generation verbose (default false) 68 | * add_desc: boolean telling if descriptions have to be added to the generated classes (default true) 69 | 70 | _generate_code_ function will create the following python files containing the GraphQL schema objects as python classes: 71 | 72 | * scalars.py -> GraphQL scalar types as python type aliases 73 | * enums.py -> GraphQL enum types as Enum classes 74 | * gql_types.py -> GraphQL object types as classes 75 | * gql_simple_types.py -> GraphQL object types as classes not using other object types 76 | * queries.py -> GraphQL query types as classes and an Enum 'Queries' containing query names as name and query classes as value 77 | * mutations.py -> mutation classes and an Enum 'Mutations' containing mutation names as name and mutation classes as value 78 | * type_refs.py -> Only in case of objects containing recursive references this file will contain references to their types 79 | 80 | 81 | ## Usage via command line 82 | 83 | ### Download of a schema as json file 84 | 85 | Example of schema retrieval command: 86 | 87 | ``` 88 | pgmcodegen download ./cmd_output/Github/schema.json -apiArgs ./downloaderArgs.json 89 | ``` 90 | 91 | This command will create the file in _./cmd_output/Github/schema.json_ containing the schema in json version using the [information](#json-args-file) given in _./downloaderArgs.json_. 92 | 93 | 94 | A few examples can be seen [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/gdbc_unittest.py#L9) and [here](https://github.com/dapalex/py-graphql-mapper/blob/develop/tests/gh_unittest.py#L13) 95 | 96 | 97 | ### Generation 98 | 99 | Classes can be generated alternatively using the CLI command: 100 | 101 | ``` 102 | pgmcodegen generate ./gdbc -apiArgs ./generatorArgs.json -v 103 | ``` 104 | 105 | This command will generate python files in _./gdbc/_ relative folder using the [information](#json-args-file) given in _./generatorArgs.json_. 106 | 107 | 108 | #### JSON args file 109 | 110 | This file is used by the generator command line interface, it contains: 111 | 112 | ```python 113 | { 114 | "addDescToGeneratedFiles": "True", # boolean telling if descriptions have to be added to the generated classes (strongly advised to be True) 115 | ##USED FOR QUERYING A GRAPHQL SERVER 116 | "apiURL": "https://mygraphqlapi.com/v2", # URL of the GraphQL server to query 117 | "httpHeaders": { # HTTP Headers necessary to query the GraphQL server 118 | "Authorization": "bearer abcdef12345678", 119 | "additionalHeader-content-type": "application/json" 120 | }, 121 | ##USED FOR GENERATE COMMAND USING A SCHEMA FILE 122 | "schemaFile": "./cmd_output/rapidapi/schema.json" # location of the json version of schema file 123 | } 124 | ``` 125 | 126 | (_apiURL_, _httpHeaders_) and _schemaFile_ are mutually exclusive. 127 | 128 | ## Using the generated files 129 | 130 | Classes generated in _mutations.py_ can be directly used for calling a GraphQL API. 131 | 132 | Supposed a generated mutation 133 | 134 | ```python 135 | 136 | class createProject(GQLMutation): 137 | """ 138 | 139 | input - Parameters for CreateProject 140 | 141 | """ 142 | class createProjectArgs(GQLArgsSet, GQLObject): 143 | """ 144 | input - Parameters for AbortQueuedMigrations 145 | 146 | """ 147 | input: CreateProjectInput ##NON NULL 148 | 149 | _args: createProjectArgs ##NON NULL 150 | type: CreateProjectPayload 151 | 152 | ``` 153 | 154 | having as input the following generated class 155 | 156 | ```python 157 | from pygqlmap.gql_types import ID 158 | 159 | class CreateProjectInput(): 160 | """ 161 | 162 | ownerId - The owner ID to create the project under. 163 | 164 | name - The name of project. 165 | 166 | body - The description of project. 167 | 168 | template - The name of the GitHub-provided template. 169 | 170 | repositoryIds - A list of repository IDs to create as linked repositories for the project 171 | 172 | clientMutationId - A unique identifier for the client performing the mutation. 173 | 174 | """ 175 | ownerId: ID ##NON NULL 176 | name: str ##NON NULL 177 | body: str 178 | template: ProjectTemplate 179 | repositoryIds: list[ID] ##NON NULL 180 | clientMutationId: str 181 | 182 | ``` 183 | 184 | The mutation class can be instantiated and defined 185 | 186 | ```python 187 | from .output.github.mutations import Mutations 188 | from .output.github.types import CreateProjectInput 189 | 190 | mutation = Mutations.createProject.value() 191 | 192 | mutation.input.ownerId = 'MDQ6VXNlcjkxMzk2ODM3' 193 | mutation.input.name = "Test create project from Mutation" + datetime.now().ctime() 194 | mutation.input.repositoryIds = ["R_kgDOH7MI4g"] 195 | ``` 196 | 197 | then _export_gql_source_ function will be able to create the following GraphQL syntax: 198 | 199 | ``` 200 | mutation MycreateProjectMutation { 201 | createProject ( input: { ownerId: "MDQ6VXNlcjkxMzk2ODM3", name: "Test create project from MutationMon Nov 21 10:19:09 2022", repositoryIds: ["R_kgDOH7MI4g"] } ) { 202 | project { 203 | id 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | Then executed using [pygqlmap](https://github.com/dapalex/py-graphql-mapper/blob/develop/pygqlmap/README.MD) 210 | 211 | ```python 212 | from pygqlmap.network import GQLResponse 213 | 214 | my_mutation = MycreateProjectMutation() 215 | my_mutation.name = 'MycreateProjectMutation' 216 | 217 | response = request("POST", graphQLServerUrl, json=json={ "query": my_mutation.export_gql_source }, headers=headers) 218 | gqlResponse = GQLResponse(response) 219 | 220 | print('Result object: ' + stringifyresult(gqlResponse.result_obj)) 221 | ``` 222 | 223 | It is possible also to create python classes manually (for a customized version for instance) using only [pygqlmap](https://github.com/dapalex/py-graphql-mapper/blob/develop/pygqlmap/README.MD) module. -------------------------------------------------------------------------------- /tests/output/re/gql_simple_types.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, List 2 | from pygqlmap.components import GQLArgsSet, GQLObject 3 | from pygqlmap.gql_types import * 4 | from pygqlmap.src.arg_builtin import * 5 | from .enums import * 6 | from .scalars import * 7 | from .type_refs import * 8 | 9 | class DeleteRefreshTokenCookie(GQLObject): 10 | deleted: bool 11 | 12 | class Refresh(GQLObject): 13 | payload: GenericScalar 14 | refreshExpiresIn: int 15 | token: str 16 | 17 | class OpenGiftCard(GQLObject): 18 | errors: list[str] 19 | 20 | class RecipientAddressInput(GQLObject): 21 | line1: str 22 | line2: str 23 | town: str 24 | postcode: str 25 | 26 | class UpdateBuyer(GQLObject): 27 | id: int 28 | firstName: str 29 | lastName: str 30 | email: str 31 | isKnown: bool 32 | isLoggedIn: bool 33 | errors: list[str] 34 | 35 | class RequestCallBackInput(GQLObject): 36 | firstName: str 37 | lastName: str 38 | phoneNumber: str 39 | companyName: str 40 | message: str 41 | pathname: str 42 | numberOfEmployees: str 43 | currentEnergySupplier: str 44 | annualConsumptionKwh: str 45 | numberOfSites: str 46 | 47 | class CreateEmployerContactInput(GQLObject): 48 | firstname: str 49 | lastname: str 50 | email: str 51 | phone: str 52 | company: str 53 | numberOfEmployees: str 54 | currentEnergySupplier: str 55 | annualConsumptionKwh: str 56 | numberOfSites: str 57 | message: str 58 | pathname: str 59 | jobTitle: str 60 | 61 | class UserContactInput(GQLObject): 62 | firstname: str 63 | lastname: str 64 | email: str 65 | 66 | class CreateContactUsMessageInput(GQLObject): 67 | name: str 68 | email: str 69 | message: str 70 | 71 | class CreateUserMemoInputs(GQLObject): 72 | note: str 73 | userId: str 74 | channel: str 75 | open: bool 76 | actionRequired: bool 77 | 78 | class SingleNewsFileUploadInput(GQLObject): 79 | file: Upload 80 | category: str 81 | newsId: str 82 | 83 | class CreateOrUpdateNewsInput(GQLObject): 84 | id: str 85 | title: str 86 | body: str 87 | hidden: bool 88 | coopId: str 89 | 90 | class ResponseInputType(GQLObject): 91 | question: str 92 | surveyResponse: str 93 | email: str 94 | multipleChoiceAnswer: list[str] 95 | radioChoiceAnswer: str 96 | textFieldAnswer: str 97 | numberAnswer: int 98 | yesNoAnswer: str 99 | surveyId: str 100 | 101 | class UpdateMemberBeneficiaryInput(GQLObject): 102 | beneficiaryId: str 103 | nominatedPerson: str 104 | email: str 105 | phone: str 106 | line1: str 107 | line2: str 108 | town: str 109 | postcode: str 110 | 111 | class AddMemberToCoopWaitingListInput(GQLObject): 112 | coopId: str 113 | generationRequestedKwh: int 114 | 115 | class ApproveMemberConsumptionEvidenceSubmissionInput(GQLObject): 116 | submissionId: str 117 | approvedEvidence: float 118 | adminNote: str 119 | 120 | class AuthLogoutSessionResponseType(GQLObject): 121 | logoutSuccessful: bool 122 | 123 | class AuthLoginSessionResponseType(GQLObject): 124 | id: int 125 | loginSuccessful: bool 126 | 127 | class UpdateUserCRMInput(GQLObject): 128 | id: int 129 | email: str 130 | 131 | class ChangeUserPasswordInput(GQLObject): 132 | currentPassword: str 133 | newPassword: str 134 | confirmNewPassword: str 135 | 136 | class PayCapacityQuoteInput(GQLObject): 137 | capacityQuoteId: int 138 | instalments: int 139 | 140 | class GetKeyValuePair(GQLObject): 141 | key: str 142 | value: str 143 | expiry: str 144 | 145 | class TokenAuthenticationInput(GQLObject): 146 | email: str 147 | password: str 148 | 149 | class SingleFileUpload(GQLObject): 150 | success: bool 151 | 152 | class UpdateQuoteInput(GQLObject): 153 | electricityConsumptionAnnual: int 154 | voucherCodes: list[str] 155 | monetaryValue: int 156 | capacity: int 157 | 158 | class UpdateSupplierQuotesInput(GQLObject): 159 | electricityAnnualStandardKwh: float 160 | hasEconomy7Meter: bool 161 | electricityAnnualDayKwh: int 162 | electricityAnnualNightKwh: int 163 | switchesGas: bool 164 | gasAnnualKwh: int 165 | hasSmartMeter: bool 166 | postcode: str 167 | isSwitchDelayed: bool 168 | switchDisabled: bool 169 | delayedSwitchDate: Date 170 | requiresManualSwitch: bool 171 | supplier: int 172 | selectedQuoteCode: str 173 | isAlreadyWithPartnerSupplier: bool 174 | isAlreadyWithBrandedSupplier: bool 175 | wantsToSeeTariffs: bool 176 | switchesToAnotherPartnerSupplier: bool 177 | electricitySupplierId: int 178 | acknowledgesTariffsWillBeDifferentInTheFuture: bool 179 | acknowledgesHeWillLoseSavingsIfHeDoesntSwitchOnTime: bool 180 | acknowledgesHeNeedsToContactTheSupplierToUpdateAddress: bool 181 | 182 | class BillingAddress(GQLObject): 183 | line1: str 184 | line2: str 185 | town: str 186 | county: str 187 | postcode: str 188 | country: str 189 | 190 | class OctopusApiAddressInput(GQLObject): 191 | line1: str 192 | line2: str 193 | line3: str 194 | town: str 195 | county: str 196 | postcode: str 197 | mpans: list[str] 198 | mprn: str 199 | gsp: str 200 | economy7: bool 201 | prepaidElectricityMeter: bool 202 | display: str 203 | 204 | class AuthenticationCreateAccountInput(GQLObject): 205 | email: str 206 | password: str 207 | path: str 208 | 209 | class BillSavingsForecastDataPoint(GQLObject): 210 | id: ID 211 | year: int 212 | pencePerKiloWattHour: float 213 | 214 | class Ambassador(GQLObject): 215 | id: ID 216 | code: str 217 | name: str 218 | showBanner: bool 219 | headline: str 220 | subtext: str 221 | backgroundColor: str 222 | textColor: str 223 | logo: str 224 | 225 | class Sender(GQLObject): 226 | id: ID 227 | isAnonymous: bool 228 | firstName: str 229 | lastName: str 230 | 231 | class Buyer(GQLObject): 232 | id: int 233 | firstName: str 234 | lastName: str 235 | email: str 236 | isKnown: bool 237 | isLoggedIn: bool 238 | 239 | class CalculateUnitsForCostInput(GQLObject): 240 | currency: str 241 | instalments: int 242 | cost: int 243 | sku: str 244 | 245 | class CalculatedPaymentLine(GQLObject): 246 | amount: int 247 | units: int 248 | netAmount: int 249 | taxAmount: int 250 | sku: str 251 | costUnits: int 252 | unitCost: int 253 | description: str 254 | message: str 255 | params: JSONString 256 | error: str 257 | 258 | class Tag(GQLObject): 259 | id: ID 260 | name: str 261 | 262 | class CalculatorMark(GQLObject): 263 | consumptionMet: float 264 | costTotal: int 265 | expectedGeneration: float 266 | 267 | class PublicGenerationDataSet(GQLObject): 268 | netPowerOutputKwh: float 269 | dateTime: str 270 | windSpeedMph: float 271 | savingsForPeriod: float 272 | estimatedCo2Saved: float 273 | estimatedTreesSaved: float 274 | 275 | class GetMemberOctopusConsumptionDataSetElement(GQLObject): 276 | consumptionKwh: float 277 | dateTime: str 278 | 279 | class Error(GQLObject): 280 | code: str 281 | message: str 282 | 283 | class SearchCountInput(GQLObject): 284 | searchTerm: str 285 | 286 | class PaginationInput(GQLObject): 287 | offset: int 288 | limit: int 289 | 290 | class GetMemberMissingInvoiceData(GQLObject): 291 | capacity: float 292 | coopId: str 293 | costOfWatts: float 294 | costOfFeesNet: float 295 | costOfFeesTax: float 296 | invoiceDate: str 297 | 298 | class VoucherPayload(GQLObject): 299 | code: str 300 | 301 | class InsightsChartDataInput(GQLObject): 302 | genFarmId: str 303 | startDate: str 304 | endDate: str 305 | period: Period 306 | 307 | class InsightsChartDataInputObject(GQLObject): 308 | genFarmId: str 309 | period: Period 310 | startDate: str 311 | endDate: str 312 | 313 | class InsightsChartDataSet(GQLObject): 314 | windSpeedMph: float 315 | generationKwh: float 316 | savings: float 317 | fromTime: DateTime 318 | toTime: DateTime 319 | 320 | class Address(GQLObject): 321 | id: ID 322 | town: str 323 | country: str 324 | 325 | class UserType(GQLObject): 326 | id: ID 327 | firstName: str 328 | lastName: str 329 | email: str 330 | phoneNumber: str 331 | 332 | class OctopusApiAddress(GQLObject): 333 | line1: str 334 | line2: str 335 | line3: str 336 | town: str 337 | county: str 338 | postcode: str 339 | mpans: list[str] 340 | mprn: str 341 | gsp: str 342 | economy7: bool 343 | prepaidElectricityMeter: bool 344 | display: str 345 | 346 | class ImageGeneral(GQLObject): 347 | id: ID 348 | name: str 349 | description: str 350 | url: str 351 | --------------------------------------------------------------------------------