sgqlc.types module¶
GraphQL Types in Python¶
This module fulfill two purposes:
declare GraphQL schema in Python, just declare classes inheriting
Type
,Interface
and fill them withField
(or base types:str
,int
,float
,bool
). You may as well declareEnum
with__choices__
orUnion
and__types__
. Then__str__()
will provide nice printout and__repr__()
will return the GraphQL declarations (which can be tweaked with__to_graphql__()
, giving indent details).__bytes__()
is also provided, mapping to a compact__to_graphql__()
version, without indent.Interpret GraphQL JSON data, by instantiating the declared classes with such information. While for scalar types it’s just a pass-thru, for
Type
andInterface
these will use the fields to provide native object with attribute or key access mapping to JSON, instead ofjson_data['key']['other']
you may useobj.key.other
. Newly declared types, such asDateTime
will take care to generate native Python objects (ie:datetime.datetime
). Setting such attributes will also update the backing store object, including converting back to valid JSON values.
These two improve usability of GraphQL a lot, pretty much like Django’s Model helps to access data bases.
Field
may be created explicitly, with information such as
target type, arguments and GraphQL name. However, more commonly these
are auto-generated by the container: GraphQL name, usually
aFieldName
will be created from Python name, usually
a_field_name
. Basic types such as int
, str
, float
or
bool
will map to Int
, String
, Float
and Boolean
.
The end-user classes and functions provided by this module are:
Schema
: top level object that will contain all declarations. For single-schema applications, you don’t have to care about this since types declared without an explicit__schema__ = SchemaInstance
member will end in theglobal_schema
.
Scalar
: “pass thru” everything received. Base for other scalar types:
Enum
: also handled as astr
, but GraphQL syntax needs them without the quotes, so special handling is done. Validation is done using__choices__
member, which is either a string (which will be splitted usingstr.split()
) or a list/tuple of strings with values.
Union
: defines the target type of a field may be one of the given__types__
.Container types:
Type
,Interface
andInput
. These are similar in usage, but GraphQL needs them defined differently. They are composed ofField
. A field may have arguments (ArgDict
), which is a set ofArg
. Arguments may contain default values orVariable
, which will be sent alongside the query (this allows to generate the query once and use variables, letting the server to use both together).
non_null()
, maps to GraphQLType!
and enforces the object is notNone
.
list_of()
, maps to GraphQL[Type]
and enforces the object is a list ofType
.
This module only provide built-in scalar types. However, three other modules will extend the behavior for common conventions:
sgqlc.types.datetime
will declareDateTime
,Date
andTime
, mapping to Python’sdatetime
. This also allows fields to be declared asmy_date = datetime.date
,
sgqlc.types.uuid
will declareUUID
, mapping to Python’suuid
. This also allows fields to be declared asmy_uuid = uuid.UUID
,
sgqlc.types.relay
will declareNode
andConnection
, matching Relay Global Object Identification and Cursor Connections, which are widely used.
Code Generator¶
If you already have schema.json
or access to a server with
introspection you may use the sgqlc-codegen schema
to
automatically generate the type definitions for you.
The generated code should be stable and can be committed to repositories,
leading to minimum diff
when updated. It may include docstrings, which
improves development experience at the expense of larger files.
- See examples:
GitHub downloads the schema using introspection and generates a schema using GraphQL descriptions as Python docstrings, see the generated github_schema.py.
Shopify downloads the schema (without descriptions) using introspection and generates a schema without Python docstrings, see the generated shopify_schema.py.
Examples¶
Common Usage¶
Common usage is to create Type
subclasses with fields without
arguments and do not use an explicit __schema__
, resulting in the
types being added to the global_schema
. Built-in scalars can be
declared using the Python classes, with sgqlc.types
classes or
with explicit Field
instances, the ContainerTypeMeta
takes care to make sure they are all instance of Field
at the
final class:
>>> class TypeUsingPython(Type):
... a_int = int
... a_float = float
... a_string = str
... a_boolean = bool
... a_id = id
... not_a_field = 1 # not a BaseType subclass or mapped python class
...
>>> TypeUsingPython # or repr(TypeUsingPython), prints out GraphQL!
type TypeUsingPython {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
>>> TypeUsingPython.a_int # or repr(Field), prints out GraphQL!
aInt: Int
>>> TypeUsingPython.a_int.name
'a_int'
>>> TypeUsingPython.a_int.graphql_name # auto-generated from name
'aInt'
>>> TypeUsingPython.a_int.type # always a :mod:`sgqlc.types` class
scalar Int
>>> TypeUsingPython.__schema__ is global_schema
True
>>> global_schema # or repr(Schema), prints out GraphQL!
schema {
scalar Int
scalar Float
scalar String
scalar Boolean
scalar ID
type TypeUsingPython {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
}
You can then use some standard Python operators to check fields in a
Type
:
>>> 'a_float' in TypeUsingPython
True
>>> 'x' in TypeUsingPython
False
>>> for field in TypeUsingPython: # iterates over :class:`Field`
... print(repr(field))
...
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
As mentioned, fields can be created with basic Python types (simpler),
with sgqlc.types
or with Field
directly:
>>> class TypeUsingSGQLC(Type):
... a_int = Int
... a_float = Float
... a_string = String
... a_boolean = Boolean
... a_id = ID
...
>>> TypeUsingSGQLC # or repr(TypeUsingSGQLC), prints out GraphQL!
type TypeUsingSGQLC {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
Using Field
instances¶
Allows for greater control, such as explicitly define the
graphql_name
instead of generating one from the Python
name
. It is also used to declare field arguments:
>>> class TypeUsingFields(Type):
... a_int = Field(int) # can use Python classes
... a_float = Field(float)
... a_string = Field(String) # or sgqlc.types classes
... a_boolean = Field(Boolean)
... a_id = Field(ID, graphql_name='anotherName') # allows customizations
... pow = Field(int, args={'base': int, 'exp': int}) # with arguments
... # more than 3 arguments renders each into new line
... many = Field(int, args={'a': int, 'b': int, 'c': int, 'd': int})
...
>>> TypeUsingFields # or repr(TypeUsingFields), prints out GraphQL!
type TypeUsingFields {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
anotherName: ID
pow(base: Int, exp: Int): Int
many(
a: Int
b: Int
c: Int
d: Int
): Int
}
Adding types to specific Schema
¶
Create a schema instance and assign as __schema__
class
member. Note that previously defined types in base_schema
are
inherited, and by default global_schema
is used as base schema:
>>> my_schema = Schema(global_schema)
>>> class MySchemaType(Type):
... __schema__ = my_schema
... i = int
...
>>> class MyOtherType(Type):
... i = int
...
>>> 'TypeUsingPython' in my_schema
True
>>> 'MySchemaType' in global_schema
False
>>> 'MySchemaType' in my_schema
True
>>> 'MyOtherType' in global_schema
True
>>> 'MyOtherType' in my_schema # added after my_schema was created!
False
>>> my_schema.MySchemaType # access types as schema attributes
type MySchemaType {
i: Int
}
>>> my_schema['MySchemaType'] # access types as schema items
type MySchemaType {
i: Int
}
>>> for t in my_schema:
... print(repr(t))
...
scalar Int
scalar Float
...
type MySchemaType {
i: Int
}
Inheritance and Interfaces¶
Inheriting another type inherits all fields:
>>> class MySubclass(TypeUsingPython):
... sub_field = int
...
>>> MySubclass
type MySubclass {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
subField: Int
}
Interfaces are similar, however they emit implements IfaceName
:
>>> class MyIface(Interface):
... sub_field = int
...
>>> MyIface
interface MyIface {
subField: Int
}
>>> class MySubclassWithIface(TypeUsingPython, MyIface):
... pass
...
>>> MySubclassWithIface
type MySubclassWithIface implements MyIface {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
subField: Int
}
Although usually types are declared first, they can be declared after interfaces as well. Note order of fields respect inheritance order:
>>> class MySubclassWithIface2(MyIface, TypeUsingPython):
... pass
...
>>> MySubclassWithIface2
type MySubclassWithIface2 implements MyIface {
subField: Int
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
Cross References (Loops)¶
If types link to themselves, declare as strings so they are lazy-evaluated:
>>> class LinkToItself(Type):
... other = Field('LinkToItself')
... non_null_other = non_null('LinkToItself')
... list_other = list_of('LinkToItself')
... list_other_non_null = list_of(non_null('LinkToItself'))
... non_null_list_other_non_null = non_null(list_of(non_null(
... 'LinkToItself')))
...
>>> LinkToItself
type LinkToItself {
other: LinkToItself
nonNullOther: LinkToItself!
listOther: [LinkToItself]
listOtherNonNull: [LinkToItself!]
nonNullListOtherNonNull: [LinkToItself!]!
}
>>> LinkToItself.other.type
type LinkToItself {
other: LinkToItself
nonNullOther: LinkToItself!
listOther: [LinkToItself]
listOtherNonNull: [LinkToItself!]
nonNullListOtherNonNull: [LinkToItself!]!
}
Also works for two unrelated types:
>>> class CrossLinkA(Type):
... other = Field('CrossLinkB')
... non_null_other = non_null('CrossLinkB')
... list_other = list_of('CrossLinkB')
... list_other_non_null = list_of(non_null('CrossLinkB'))
... non_null_list_other_non_null = non_null(list_of(non_null(
... 'CrossLinkB')))
...
>>> class CrossLinkB(Type):
... other = Field('CrossLinkA')
... non_null_other = non_null('CrossLinkA')
... list_other = list_of('CrossLinkA')
... list_other_non_null = list_of(non_null('CrossLinkA'))
... non_null_list_other_non_null = non_null(list_of(non_null(
... 'CrossLinkA')))
...
>>> CrossLinkA
type CrossLinkA {
other: CrossLinkB
nonNullOther: CrossLinkB!
listOther: [CrossLinkB]
listOtherNonNull: [CrossLinkB!]
nonNullListOtherNonNull: [CrossLinkB!]!
}
>>> CrossLinkB
type CrossLinkB {
other: CrossLinkA
nonNullOther: CrossLinkA!
listOther: [CrossLinkA]
listOtherNonNull: [CrossLinkA!]
nonNullListOtherNonNull: [CrossLinkA!]!
}
>>> CrossLinkA.other.type
type CrossLinkB {
other: CrossLinkA
nonNullOther: CrossLinkA!
listOther: [CrossLinkA]
listOtherNonNull: [CrossLinkA!]
nonNullListOtherNonNull: [CrossLinkA!]!
}
>>> CrossLinkB.other.type
type CrossLinkA {
other: CrossLinkB
nonNullOther: CrossLinkB!
listOther: [CrossLinkB]
listOtherNonNull: [CrossLinkB!]
nonNullListOtherNonNull: [CrossLinkB!]!
}
Special Attribute Names¶
Attributes starting with _
are ignored, however if for some reason
you must use such attribute name, then declare ALL attributes
in that class (no need to repeat inherited attributes from interfaces)
using the __field_names__
, which should have a tuple of strings:
>>> class TypeUsingSpecialAttributes(Type):
... __field_names__ = ('_int', '_two_words')
... _int = int
... _two_words = str
... not_handled = float # not declared!
...
>>> TypeUsingSpecialAttributes # or repr(TypeUsingSpecialAttributes)
type TypeUsingSpecialAttributes {
_int: Int
_twoWords: String
}
>>> TypeUsingSpecialAttributes._int # or repr(Field), prints out GraphQL!
_int: Int
>>> TypeUsingSpecialAttributes._int.name
'_int'
>>> TypeUsingSpecialAttributes._int.graphql_name # auto-generated from name
'_int'
Note that while the leading underscores (_
) are preserved, the rest of
internal underscores are converted to camel case:
>>> TypeUsingSpecialAttributes._two_words
_twoWords: String
>>> TypeUsingSpecialAttributes._two_words.name
'_two_words'
>>> TypeUsingSpecialAttributes._two_words.graphql_name
'_twoWords'
Note
Take care with the double underscores
__
as Python mangles the name with the class name in order to “protect” and it will result inAttributeError
Note that undeclared fields won’t be handled, but they still exist as regular
python attributes, in this case it references the float
class:
>>> TypeUsingSpecialAttributes.not_handled
<class 'float'>
Non GraphQL Attributes¶
The __field_names__
may also be used to allow non-GraphQL
attributes that would otherwise be handled as such, this
explicitly limits the scope where SGQLC will handle.
Utilities¶
One can obtain fields as container attributes or items:
>>> TypeUsingPython.a_int
aInt: Int
>>> TypeUsingPython['a_int']
aInt: Int
However they raise exceptions if doesn’t exist:
>>> TypeUsingPython.does_not_exist
Traceback (most recent call last):
...
AttributeError: TypeUsingPython has no field does_not_exist
>>> TypeUsingPython['does_not_exist']
Traceback (most recent call last):
...
KeyError: 'TypeUsingPython has no field does_not_exist'
Fields show in dir()
alongside with non-fields (sorted):
>>> for name in dir(TypeUsingPython):
... if not name.startswith('_'):
... print(name)
a_boolean
a_float
a_id
a_int
a_string
not_a_field
Unless non_null()
is used, containers can be created for
None
:
>>> TypeUsingPython(None)
TypeUsingPython()
>>> TypeUsingPython.__to_json_value__(None) # returns None
For instances, field values can be obtained or set attributes or items, when setting a known field, it also updates the backing store:
>>> json_data = {'aInt': 1}
>>> obj = TypeUsingPython(json_data)
>>> obj.a_int
1
>>> obj['a_int']
1
>>> obj.a_int = 2
>>> json_data['aInt']
2
>>> obj['a_int'] = 3
>>> json_data['aInt']
3
>>> obj['a_float'] = 2.1 # known field!
>>> json_data['aFloat']
2.1
>>> obj.a_float = 3.3 # known field!
>>> json_data['aFloat']
3.3
Unknown fields raise exceptions when obtained, but are allowed to be set, however doesn’t update the backing store:
>>> obj.does_not_exist
Traceback (most recent call last):
...
AttributeError: 'TypeUsingPython' object has no attribute 'does_not_exist'
>>> obj['does_not_exist']
Traceback (most recent call last):
...
KeyError: 'TypeUsingPython(a_int=3, a_float=3.3) has no field does_not_exist'
>>> obj['does_not_exist'] = 'abc' # unknown field, no updates to json_data
>>> json_data['does_not_exist']
Traceback (most recent call last):
...
KeyError: 'does_not_exist'
While repr()
prints out summary in Python-friendly syntax, bytes()
can be used to get compressed JSON with sorted keys:
>>> print(repr(obj))
TypeUsingPython(a_int=3, a_float=3.3)
>>> print(bytes(obj).decode('utf-8'))
{"aFloat":3.3,"aInt":3}
- license:
ISC
- class sgqlc.types.Arg(typ, graphql_name=None, default=None)[source]¶
Bases:
BaseItem
GraphQL
Field
argument.>>> class MyTypeWithArgument(Type): ... a = Field(str, args={'arg_name': int}) # implicit ... b = Field(str, args={'arg': Arg(int)}) # explicit + Python ... c = Field(str, args={'arg': Arg(Int)}) # explicit + sgqlc.types ... d = Field(str, args={'arg': Arg(int, default=1)}) ... >>> MyTypeWithArgument type MyTypeWithArgument { a(argName: Int): String b(arg: Int): String c(arg: Int): String d(arg: Int = 1): String }
- __init__(typ, graphql_name=None, default=None)[source]¶
- Parameters:
typ (
Scalar
,Type
or str) – theScalar
orType
derived class. If this would cause a cross reference and the other type is not declared yet, then use the string name to query in the schema.graphql_name (str) – the name to use in JSON object, usually
aName
. IfNone
or empty, will be created from python, convertinga_name
toaName
usingBaseItem._to_graphql_name()
default – The default value for field. May be a value or
Variable
.
- class sgqlc.types.ArgDict(*lst, **mapping)[source]¶
Bases:
OrderedDict
The
Field
Argument Dict.Common usage is inside
Field
:>>> class MyType(Type): ... a = Field(Int, args={'argument1': String}) # implicit ... b = Field(Int, args=ArgDict(argument1=String)) # explicit ... >>> print(repr(MyType)) type MyType { a(argument1: String): Int b(argument1: String): Int } >>> print(repr(MyType.a)) a(argument1: String): Int >>> print(repr(MyType.a.args)) (argument1: String) >>> print(repr(MyType.b)) b(argument1: String): Int >>> print(repr(MyType.b.args)) (argument1: String) >>> print(repr(MyType.b.args['argument1'])) argument1: String >>> print(bytes(MyType.b.args['argument1']).decode('utf-8')) argument1: String
This takes care to ensure values are
Arg
. In the example above, we’re not passingArg
, rather just a type (String
) and it’s working internally to createArg
. For ease of use, can be created in various forms. Note they must be added to a container field to be useful, which would callArgDict._set_container()
for you, here called manually for testing purposes:>>> ad = ArgDict(name=str) >>> ad._set_container(global_schema, None) # done automatically by Field >>> print(ad) (name: String)
>>> ad = ArgDict(name=String) >>> ad._set_container(global_schema, None) # done automatically by Field >>> print(ad) (name: String)
>>> ad = ArgDict({'name': str}) >>> ad._set_container(global_schema, None) # done automatically by Field >>> print(ad) (name: String)
>>> ad = ArgDict(('name', str), ('other', int)) >>> ad._set_container(global_schema, None) # done automatically by Field >>> print(ad) (name: String, other: Int)
>>> ad = ArgDict((('name', str), ('other', int))) >>> ad._set_container(global_schema, None) # done automatically by Field >>> print(ad) (name: String, other: Int)
Note that for better understanding, more than 3 arguments are printed in multiple lines:
>>> ad = ArgDict(a=int, b=float, c=non_null(str), d=list_of(int)) >>> ad._set_container(global_schema, None) # done automatically by Field >>> print(ad) ( a: Int b: Float c: String! d: [Int] ) >>> print(bytes(ad).decode('utf-8')) ( a: Int b: Float c: String! d: [Int] )
This is also the case for input values:
>>> print('fieldName' + ad.__to_graphql_input__({ ... 'a': 1, 'b': 2.2, 'c': 'hi', 'd': [1, 2], ... })) fieldName( a: 1 b: 2.2 c: "hi" d: [1, 2] )
Variables can be handled using
Variable
instances:>>> print('fieldName' + ad.__to_graphql_input__({ ... 'a': Variable('a'), ... 'b': Variable('b'), ... 'c': Variable('c'), ... 'd': Variable('d'), ... })) fieldName( a: $a b: $b c: $c d: $d )
- class sgqlc.types.BaseItem(typ, graphql_name=None)[source]¶
Bases:
object
Each parameter has a GraphQL type, such as a derived class from
Scalar
orType
, this is used for nesting, conversion to native Python types, generating queries, etc.- __init__(typ, graphql_name=None)[source]¶
- Parameters:
typ (
Scalar
,Type
or str) – theScalar
orType
derived class. If this would cause a cross reference and the other type is not declared yet, then use the string name to query in the schema.graphql_name (str) – the name to use in JSON object, usually
aName
. IfNone
or empty, will be created from python, convertinga_name
toaName
usingArg._to_graphql_name()
- classmethod _to_graphql_name(name)[source]¶
Converts a Python name,
a_name
to GraphQL:aName
.Note that leading underscores (
_
) are preserved.>>> BaseItem._to_graphql_name('a_name') 'aName' >>> BaseItem._to_graphql_name('__underscore_prefixed') '__underscorePrefixed' >>> BaseItem._to_graphql_name('__typename__') '__typename'
- classmethod _to_python_name(graphql_name)[source]¶
Converts a GraphQL name,
aName
to Python:a_name
.Note that an underscore is appended if the name is a Python keyword.
>>> BaseItem._to_python_name('aName') 'a_name' >>> BaseItem._to_python_name('for') 'for_' >>> BaseItem._to_python_name('__typename') '__typename__'
- class sgqlc.types.BaseMeta(name, bases, namespace)[source]¶
Bases:
type
Automatically adds class to its schema
- class sgqlc.types.BaseType[source]¶
Bases:
object
Base shared by all GraphQL classes.
- __weakref__¶
list of weak references to the object (if defined)
- class sgqlc.types.Boolean(json_data, selection_list=None)[source]¶
Bases:
Scalar
Maps GraphQL
Boolean
to Pythonbool
.
- class sgqlc.types.ContainerType(json_data, selection_list=None)[source]¶
Bases:
BaseTypeWithTypename
Container of
Field
.For ease of use, fields can be declared by sub classes in the following ways:
name = str
to create a simple string field. Other basic types are allowed as well:int
,float
,str
,bool
,uuid.UUID
,datetime.time
,datetime.date
anddatetime.datetime
. These are only used as identifiers to translate usingmap_python_to_graphql
dict. Note thatid
, although is not a type, maps toID
.name = TypeName
for subclasses ofBaseType
, such as pre-defined scalars (Int
, etc) or your own defined types, fromType
.name = Field(TypeName, graphql_name='differentName', args={...})
to explicitly define more field information, such as GraphQL JSON name, query parameters, etc.
The metaclass
ContainerTypeMeta
will normalize all of those members to be instances ofField
, as well as provide useful container protocol such as__contains__
,__getitem__
,__iter__
and so on.Fields from all bases (interfaces, etc) are merged.
Members started with underscore (
_
) are not processed.- __contains__(name)[source]¶
Checks if for a known field name in the instance.
Unlike
name in SubclassOfType
, which checks amongst all declared fields, this matches only fields that exist in the object, based on thejson_data
used to create the object, and the one that provides the backing store:>>> json_data = { 'aInt': 1, 'aFloat': 2.1 } >>> obj = global_schema.TypeUsingPython(json_data) >>> 'a_int' in obj True >>> 'a_float' in obj True >>> 'a_string' in obj # in class, but not instance False >>> 'a_string' in obj.__class__ True
After it’s set for the given instance, then becomes true:
>>> obj.a_string = 'hello world' # known field >>> 'a_string' in obj # now in instance True
- __getitem__(name)[source]¶
Get the field given its name.
Considering
TypeUsingPython
, previously declared in the module documentation:>>> global_schema.TypeUsingPython['a_int'] aInt: Int
>>> global_schema.TypeUsingPython['unknown_field'] Traceback (most recent call last): ... KeyError: 'TypeUsingPython has no field unknown_field'
- __iter__()[source]¶
Iterate over known fields of the instance.
Unlike
iter(SubclassOfType)
, which iterates over all declared fields, this iterator matches only fields that exist in the object, based on thejson_data
used to create the object, and the one that provides the backing store:>>> json_data = { 'aInt': 1, 'aFloat': 2.1 } >>> obj = global_schema.TypeUsingPython(json_data) >>> for field_name in obj: ... print(field_name, repr(obj[field_name])) a_int 1 a_float 2.1 >>> for field in obj.__class__: ... print(repr(field)) aInt: Int aFloat: Float aString: String aBoolean: Boolean aId: ID
After it’s set for the given instance, then it’s included in the iterator:
>>> obj.a_string = 'hello world' # known field >>> for field_name in obj: ... print(field_name, repr(obj[field_name])) a_int 1 a_float 2.1 a_string 'hello world'
However that’s valid for known
Field
for the givenContainerType
subclasses:>>> obj.new_attr = 'some value' # unknown field, not in 'iter' >>> for field_name in obj: ... print(field_name, repr(obj[field_name])) a_int 1 a_float 2.1 a_string 'hello world'
- __len__()[source]¶
Checks how many fields are set in the instance.
>>> json_data = { 'aInt': 1, 'aFloat': 2.1 } >>> obj = global_schema.TypeUsingPython(json_data) >>> len(obj) 2
>>> obj.a_string = 'hello world' # known field >>> len(obj) 3
- __setattr__(name, value)[source]¶
Sets the attribute value, if a
Field
updates backing store.Considering
TypeUsingPython
, previously declared in the module documentation:>>> json_data = {'aInt': 1, 'aFloat': 2.1} >>> obj = global_schema.TypeUsingPython(json_data) >>> obj.a_int, obj.a_float (1, 2.1) >>> obj.a_int = 123 >>> obj.a_int, obj.a_float (123, 2.1) >>> json_data['aInt'] 123
However that’s valid for known
Field
for the givenContainerType
subclasses:>>> obj.new_attr = 'some value' # no field, no backing store updates >>> obj.new_attr 'some value' >>> json_data['new_attr'] Traceback (most recent call last): ... KeyError: 'new_attr' >>> json_data['newAttr'] Traceback (most recent call last): ... KeyError: 'newAttr'
- class sgqlc.types.ContainerTypeMeta(name, bases, namespace)[source]¶
Bases:
BaseMetaWithTypename
Creates container types, ensures fields are instance of Field.
- class sgqlc.types.Enum(json_data, selection_list=None)[source]¶
Bases:
BaseType
This is an abstract class that enumerations should inherit and define
__choices__
class member with a list of strings matching the choices allowed by this enumeration. A single string may also be used, in such case it will be split usingstr.split()
.Note that
__choices__
is not set in the final class, the metaclass will use that to build members and provide the__iter__
,__contains__
and__len__
instead.The instance constructor will never return instance of
Enum
, rather the string, if that matches.Examples:
>>> class Colors(Enum): ... __choices__ = ('RED', 'GREEN', 'BLUE') ... >>> Colors('RED') 'RED' >>> Colors(None) # returns None >>> Colors('MAGENTA') Traceback (most recent call last): ... ValueError: Colors does not accept value MAGENTA
Using a string will automatically split and convert to tuple:
>>> class Fruits(Enum): ... __choices__ = 'APPLE ORANGE BANANA' ... >>> Fruits.__choices__ ('APPLE', 'ORANGE', 'BANANA') >>> len(Fruits) 3
Enumerations have a special syntax in GraphQL, no quotes:
>>> print(Fruits.__to_graphql_input__(Fruits.APPLE)) APPLE >>> print(Fruits.__to_graphql_input__(None)) null
And for JSON it’s a string as well (so JSON encoder adds quotes):
>>> print(json.dumps(Fruits.__to_json_value__(Fruits.APPLE))) "APPLE"
Variables are passed thru:
>>> Fruits(Variable('var')) $var
- class sgqlc.types.Field(typ, graphql_name=None, args=None)[source]¶
Bases:
BaseItem
Field in a
Type
container.Each field has a GraphQL type, such as a derived class from
Scalar
orType
, this is used for nesting, conversion to native Python types, generating queries, etc.- __bytes__()[source]¶
Prints GraphQL without indentation.
>>> print(repr(global_schema.TypeUsingFields.many)) many( a: Int b: Int c: Int d: Int ): Int >>> print(bytes(global_schema.TypeUsingFields.many).decode('utf-8')) many( a: Int b: Int c: Int d: Int ): Int
- __init__(typ, graphql_name=None, args=None)[source]¶
- Parameters:
typ (
Scalar
,Type
or str) – theScalar
orType
derived class. If this would cause a cross reference and the other type is not declared yet, then use the string name to query in the schema.graphql_name (str) – the name to use in JSON object, usually
aName
. IfNone
or empty, will be created from python, convertinga_name
toaName
usingBaseItem._to_graphql_name()
args (
ArgDict
) – The field parameters as aArgDict
or compatible type (dict, or iterable of key-value pairs). The value may be a mapped Python type (ie:str
), explicit type (ie:String
), type name (ie:"String"
, to allow cross references) orArg
instances.
- class sgqlc.types.Float(json_data, selection_list=None)[source]¶
Bases:
Scalar
Maps GraphQL
Float
to Pythonfloat
.
- class sgqlc.types.ID(json_data, selection_list=None)[source]¶
Bases:
Scalar
Maps GraphQL
ID
to Pythonstr
.
- class sgqlc.types.Input(*args, **kwargs)[source]¶
Bases:
ContainerType
GraphQL
input Name
.Input types are similar to
Type
, but they are used as argument values. They have more restrictions, such as noInterface
,Union
orType
are allowed as field types. Only scalars orInput
.Note
SGQLC currently doesn’t enforce the field type restrictions imposed by the server.
>>> class MyInput(Input): ... a_int = int ... a_float = float ... >>> MyInput input MyInput { aInt: Int aFloat: Float } >>> print(MyInput.__to_graphql_input__({'a_int': 1, 'a_float': 2.2})) {aInt: 1, aFloat: 2.2}
>>> a_var = Variable('input') >>> print(MyInput.__to_graphql_input__(a_var)) $input
- __init__(_json_obj=None, _selection_list=None, **kwargs)[source]¶
Create the type given a json object or keyword arguments.
>>> class AnotherInput(Input): ... a_str = str ... >>> class TheInput(Input): ... a_int = int ... a_float = float ... a_nested = AnotherInput ... a_nested_list = list_of(AnotherInput) ...
It can be constructed using fields and values as a regular Python class:
>>> TheInput(a_int=1, a_float=1.2, a_nested=AnotherInput(a_str='hi')) TheInput(a_int=1, a_float=1.2, a_nested=AnotherInput(a_str='hi'))
Or can be given as a dict (ie: JSON data) as parameter:
>>> TheInput({'aInt': 1, 'aFloat': 1.2, 'aNested': {'aStr': 'hi'}}) TheInput(a_int=1, a_float=1.2, a_nested=AnotherInput(a_str='hi'))
It can be printed to GraphQL DSL using
__to_graphql_input__()
:>>> value = TheInput(a_int=1, a_float=1.2, ... a_nested=AnotherInput(a_str='hi'), ... a_nested_list=[AnotherInput(a_str='there')]) >>> print(TheInput.__to_graphql_input__(value)) {aInt: 1, aFloat: 1.2, aNested: {aStr: "hi"}, aNestedList: [{aStr: "there"}]} >>> value = TheInput({'aInt': 1, 'aFloat': 1.2, 'aNested': {'aStr': 'hi'}, ... 'aNestedList': [{'aStr': 'there'}]}) >>> print(TheInput.__to_graphql_input__(value)) {aInt: 1, aFloat: 1.2, aNested: {aStr: "hi"}, aNestedList: [{aStr: "there"}]}
The nested types (lists, non-null) can also take an already realized value, see
AnotherInput
below:>>> value = TheInput({'aInt': 1, 'aFloat': 1.2, 'aNested': {'aStr': 'hi'}, ... 'aNestedList': [AnotherInput(a_str='there')]}) >>> print(TheInput.__to_graphql_input__(value)) {aInt: 1, aFloat: 1.2, aNested: {aStr: "hi"}, aNestedList: [{aStr: "there"}]}
Input types can be constructed from variables. Note that the input variable can’t be an element of a list, the list itself must be a variable:
>>> a_var = Variable('input') >>> value = TheInput(a_var) >>> print(TheInput.__to_graphql_input__(value)) $input >>> value = TheInput(a_int=1, a_float=1.2, ... a_nested=a_var, ... a_nested_list=[AnotherInput(a_str='there')]) >>> print(TheInput.__to_graphql_input__(value)) {aInt: 1, aFloat: 1.2, aNested: $input, aNestedList: [{aStr: "there"}]} >>> value = TheInput(a_int=1, a_float=1.2, ... a_nested=AnotherInput(a_str='hi'), ... a_nested_list=a_var) >>> print(TheInput.__to_graphql_input__(value)) {aInt: 1, aFloat: 1.2, aNested: {aStr: "hi"}, aNestedList: $input}
None
will printnull
:>>> print(TheInput.__to_graphql_input__(None)) null
>>> value = TheInput(a_int=None, a_float=None, a_nested=None, ... a_nested_list=None) >>> print(TheInput.__to_graphql_input__(value)) {aInt: null, aFloat: null, aNested: null, aNestedList: null}
Unless fields are non-nullable, then
ValueError
is raised:>>> class TheNonNullInput(Input): ... a_int = non_null(int) ... a_float = non_null(float) ... a_nested = non_null(AnotherInput) ... a_nested_list = non_null(list_of(non_null(AnotherInput)))
>>> TheNonNullInput(a_int=None, a_float=1.2, ... a_nested=AnotherInput(a_str='hi'), ... a_nested_list=[AnotherInput(a_str='there')] ... ) Traceback (most recent call last): ... ValueError: Int! received null value >>> TheNonNullInput(a_int=1, a_float=None, ... a_nested=AnotherInput(a_str='hi'), ... a_nested_list=[AnotherInput(a_str='there')] ... ) Traceback (most recent call last): ... ValueError: Float! received null value >>> TheNonNullInput(a_int=1, a_float=1.2, ... a_nested=None, ... a_nested_list=[AnotherInput(a_str='there')] ... ) Traceback (most recent call last): ... ValueError: AnotherInput! received null value >>> TheNonNullInput(a_int=1, a_float=1.2, ... a_nested=AnotherInput(a_str='hi'), ... a_nested_list=[None] ... ) Traceback (most recent call last): ... ValueError: AnotherInput! received null value >>> TheNonNullInput(a_int=1, a_float=1.2, ... a_nested=AnotherInput(a_str='hi'), ... a_nested_list=None ... ) Traceback (most recent call last): ... ValueError: [AnotherInput!]! received null value
Note
selection_list
parameter makes no sense and is ignored, it’s only provided to cope with theContainerType
interface.
- class sgqlc.types.Int(json_data, selection_list=None)[source]¶
Bases:
Scalar
Maps GraphQL
Int
to Pythonint
.>>> Int # or repr() scalar Int >>> str(Int) 'Int' >>> bytes(Int) b'scalar Int'
- class sgqlc.types.Interface(*args, **kwargs)[source]¶
Bases:
ContainerType
GraphQL
interface Name
.If the subclass also adds
Interface
to the class declarations, then it will emitinterface Name implements Iface1, Iface2
, also making their fields automatically available in the final class.Whenever interfaces are instantiated, if there is a
__typename
injson_data
and the type is known, it will automatically create the more specific type. Otherwise it instantiates the interface itself:>>> class SomeIface(Interface): ... i = int ... >>> class TypeWithIface(Type, SomeIface): ... pass ... >>> data = {'__typename': 'TypeWithIface', 'i': 123} >>> SomeIface(data) TypeWithIface(i=123) >>> data = {'__typename': 'UnknownType', 'i': 123} >>> SomeIface(data) SomeIface(i=123)
- class sgqlc.types.Scalar(json_data, selection_list=None)[source]¶
Bases:
BaseType
Basic scalar types, passed thru (no conversion).
This may be used directly if no special checks or conversions are needed. Otherwise use subclasses, like
Int
,Float
,String
,Boolean
,ID
…Scalar classes will never produce instance of themselves, rather return the converted value (int, bool…)
>>> class MyTypeWithScalar(Type): ... v = Scalar ... >>> MyTypeWithScalar({'v': 1}).v 1 >>> MyTypeWithScalar({'v': 'abc'}).v 'abc'
Variables are passed thru:
>>> MyTypeWithScalar({'v': Variable('var')}).v $var
- class sgqlc.types.Schema(base_schema=None)[source]¶
Bases:
object
The schema will contain declared types.
There is a default schema called
global_schema
, a singleton that is automatically assigned to every type that does not provide its own schema.Once types are constructed, they are automatically added to the schema as properties of the same name, for example
Int
is exposed asschema.Int
,schema['Int']
orschema.scalar['Int']
.New schema will inherit the types defined at
base_schema
, which defaults toglobal_schema
, at the time of their creation. However types added tobase_schema
after the schema creation are not automatically picked by existing schema. The copy happens at construction time.New types may be added to schema using
schema += type
and removed withschema -= type
. However those will not affect their membertype.__schema__
, which remains the same (where they where originally created).The schema is an iterator that will report all registered types.
- __bytes__()[source]¶
GraphQL schema without indentation.
>>> print(bytes(global_schema).decode('utf-8')) schema { scalar Int scalar Float scalar String ... }
- __contains__(key)[source]¶
Checks if the type name is known in this schema.
Considering
TypeUsingPython
, previously declared in the module documentation:>>> 'TypeUsingPython' in global_schema True >>> 'UnknownTypeName' in global_schema False
- __getattr__(key)[source]¶
Get the type using schema attribute.
Considering
TypeUsingPython
, previously declared in the module documentation:>>> global_schema.TypeUsingPython type TypeUsingPython { aInt: Int aFloat: Float aString: String aBoolean: Boolean aId: ID }
>>> global_schema.UnknownTypeName Traceback (most recent call last): ... AttributeError: UnknownTypeName
One can use
Schema.kind.Type
syntax as well, it exposes anODict
object:>>> global_schema.scalar.Int scalar Int >>> global_schema.scalar['Int'] scalar Int >>> global_schema.scalar.UnknownTypeName Traceback (most recent call last): ... AttributeError: ... has no field UnknownTypeName >>> global_schema.type.TypeUsingPython type TypeUsingPython { ... >>> for t in global_schema.type.values(): ... print(repr(t)) ... type TypeUsingPython { ... type TypeUsingSGQLC { ... type TypeUsingFields { ... type MyOtherType { ... type MyType { ... }
- __getitem__(key)[source]¶
Get the type given its name.
Considering
TypeUsingPython
, previously declared in the module documentation:>>> global_schema['TypeUsingPython'] type TypeUsingPython { aInt: Int aFloat: Float aString: String aBoolean: Boolean aId: ID }
>>> global_schema['UnknownTypeName'] Traceback (most recent call last): ... KeyError: 'UnknownTypeName'
- __iadd__(typ)[source]¶
Manually add a type to the schema.
Types are automatically once their class is created. Only use this if you’re copying a type from one schema to another.
Note that the type name
str(typ)
must not exist in the schema, otherwiseValueError
is raised.To remove a type, use
schema -= typ
.As explained in the
sgqlc.types
documentation, the newly created schema will inherit types from the base schema only at creation time:>>> my_schema = Schema(global_schema) >>> class MySchemaType(Type): ... __schema__ = my_schema ... i = int ... >>> 'MySchemaType' in global_schema False >>> 'MySchemaType' in my_schema True
But
__iadd__
and__isub__
can be used to add or remove types:>>> global_schema += MySchemaType >>> 'MySchemaType' in global_schema True >>> global_schema -= MySchemaType >>> 'MySchemaType' in global_schema False
Note that different type with the same name can’t be added:
>>> my_schema2 = Schema(global_schema) >>> class MySchemaType(Type): # redefining, different schema: ok ... __schema__ = my_schema2 ... f = float ... >>> my_schema += MySchemaType Traceback (most recent call last): ... ValueError: Schema already has MySchemaType=MySchemaType
- __isub__(typ)[source]¶
Remove a type from the schema.
This may be of use to override some type, such as
sgqlc.types.datetime.Date
orsgqlc.types.datetime.DateTime
.
- class sgqlc.types.String(json_data, selection_list=None)[source]¶
Bases:
Scalar
Maps GraphQL
String
to Pythonstr
.
- class sgqlc.types.Type(json_data, selection_list=None)[source]¶
Bases:
ContainerType
GraphQL
type Name
.If the subclass also adds
Interface
to the class declarations, then it will emittype Name implements Iface1, Iface2
, also making their fields automatically available in the final class.
- class sgqlc.types.Union(json_data, selection_list=None)[source]¶
Bases:
BaseTypeWithTypename
This is an abstract class that union of multiple types should inherit and define
__types__
, a list of pre-definedType
.>>> class IntOrFloatOrString(Union): ... __types__ = (Int, float, 'String') ... >>> IntOrFloatOrString # or repr(), prints out GraphQL! union IntOrFloatOrString = Int | Float | String >>> Int in IntOrFloatOrString True >>> 'Int' in IntOrFloatOrString # may use type names as well True >>> int in IntOrFloatOrString # may use native Python types as well True >>> ID in IntOrFloatOrString False >>> len(IntOrFloatOrString) 3 >>> for t in IntOrFloatOrString: ... print(repr(t)) scalar Int scalar Float scalar String
Failing to define types will raise exception:
>>> class FailureUnion(Union): ... pass Traceback (most recent call last): ... ValueError: FailureUnion: missing __types__
Whenever instantiating the type, pass a JSON object with
__typename
(done automatically using fragments via__as__
):>>> class TypeA(Type): ... i = int ... >>> class TypeB(Type): ... s = str ... >>> class TypeU(Union): ... __types__ = (TypeA, TypeB) ... >>> data = {'__typename': 'TypeA', 'i': 1} >>> TypeU(data) TypeA(i=1) >>> data = {'__typename': 'TypeB', 's': 'hi'} >>> TypeU(data) TypeB(s='hi')
It nicely handles unknown types:
>>> data = {'v': 123} >>> TypeU(data) # no __typename UnknownType() >>> data = {'__typename': 'TypeUnknown', 'v': 123} >>> TypeU(data) # auto-generates empty types TypeUnknown() >>> data = None >>> TypeU(data)
Variables are passed thru:
>>> TypeU(Variable('var')) $var
- class sgqlc.types.Variable(name, graphql_name=None)[source]¶
Bases:
object
GraphQL variable:
$varName
Usually given as
Arg
default value:>>> class MyTypeWithVariable(Type): ... f = Field(str, args={'first': Arg(int, default=Variable('var'))}) ... >>> MyTypeWithVariable type MyTypeWithVariable { f(first: Int = $var): String } >>> print(repr(MyTypeWithVariable.f.args['first'].default)) $var >>> print(str(MyTypeWithVariable.f.args['first'].default)) $var >>> print(bytes(MyTypeWithVariable.f.args['first'].default).decode('utf8')) $var
- sgqlc.types.list_of(t)[source]¶
Generates list of types ([t])
The example below highlights the usage including its usage with lists:
non_null_list_of_int
means it must be a list, notNone
, however list elements may beNone
, ie:[None, 1, None, 2]
;list_of_non_null_int
means it may beNone
or be a list, however list elements must not beNone
, ie:None
or[1, 2]
;non_null_list_of_non_null_int
means it must be a list, notNone
and the list elements must not beNone
, ie:[1, 2]
.
>>> class TypeWithListFields(Type): ... list_of_int = list_of(int) ... list_of_float = list_of(Float) ... list_of_string = Field(list_of(String)) ... non_null_list_of_int = non_null(list_of(int)) ... list_of_non_null_int = list_of(non_null(int)) ... non_null_list_of_non_null_int = non_null(list_of(non_null(int))) ... >>> TypeWithListFields type TypeWithListFields { listOfInt: [Int] listOfFloat: [Float] listOfString: [String] nonNullListOfInt: [Int]! listOfNonNullInt: [Int!] nonNullListOfNonNullInt: [Int!]! }
It takes care to enforce proper type, including non-null checking on its elements when creating instances. Giving proper JSON data:
>>> json_data = { ... 'listOfInt': [1, 2], ... 'listOfFloat': [1.1, 2.1], ... 'listOfString': ['hello', 'world'], ... 'nonNullListOfInt': [None, 1, None, 2], ... 'listOfNonNullInt': [1, 2, 3], ... 'nonNullListOfNonNullInt': [1, 2, 3, 4], ... } >>> obj = TypeWithListFields(json_data) >>> for field_name in obj: ... print(field_name, repr(obj[field_name])) ... list_of_int [1, 2] list_of_float [1.1, 2.1] list_of_string ['hello', 'world'] non_null_list_of_int [None, 1, None, 2] list_of_non_null_int [1, 2, 3] non_null_list_of_non_null_int [1, 2, 3, 4]
Note that lists that are not enclosed in
non_null()
can beNone
:>>> json_data = { ... 'listOfInt': None, ... 'listOfFloat': None, ... 'listOfString': None, ... 'nonNullListOfInt': [None, 1, None, 2], ... 'listOfNonNullInt': None, ... 'nonNullListOfNonNullInt': [1, 2, 3], ... } >>> obj = TypeWithListFields(json_data) >>> for field_name in obj: ... print(field_name, repr(obj[field_name])) ... list_of_int None list_of_float None list_of_string None non_null_list_of_int [None, 1, None, 2] list_of_non_null_int None non_null_list_of_non_null_int [1, 2, 3]
Types will be converted, so although not usual (since GraphQL gives you the proper JSON type), this can be done:
>>> json_data = { ... 'listOfInt': ['1', '2'], ... 'listOfFloat': [1, '2.1'], ... 'listOfString': ['hello', 2], ... 'nonNullListOfInt': [None, '1', None, 2.1], ... 'listOfNonNullInt': ['1', 2.1, 3], ... 'nonNullListOfNonNullInt': ['1', 2.1, 3, 4], ... } >>> obj = TypeWithListFields(json_data) >>> for field_name in obj: ... print(field_name, repr(obj[field_name])) ... list_of_int [1, 2] list_of_float [1.0, 2.1] list_of_string ['hello', '2'] non_null_list_of_int [None, 1, None, 2] list_of_non_null_int [1, 2, 3] non_null_list_of_non_null_int [1, 2, 3, 4]
Giving incorrect (nonconvertible) JSON data will raise exceptions:
>>> json_data = { 'listOfInt': 1 } >>> obj = TypeWithListFields(json_data) Traceback (most recent call last): ... ValueError: TypeWithListFields selection 'list_of_int': ...
>>> json_data = { 'listOfInt': ['x'] } >>> obj = TypeWithListFields(json_data) Traceback (most recent call last): ... ValueError: TypeWithListFields selection 'list_of_int': ...
>>> json_data = { 'listOfNonNullInt': [1, None] } >>> obj = TypeWithListFields(json_data) Traceback (most recent call last): ... ValueError: TypeWithListFields selection 'list_of_non_null_int': ...
Lists are usable as input types as well:
>>> class TypeWithListInput(Type): ... a = Field(str, args={'values': Arg(list_of(int), default=[1, 2])}) ... b = Field(str, args={'values': Arg(list_of(int))}) ... >>> TypeWithListInput type TypeWithListInput { a(values: [Int] = [1, 2]): String b(values: [Int]): String }
>>> print(json.dumps(list_of(int).__to_json_value__([1, 2]))) [1, 2] >>> print(json.dumps(list_of(int).__to_json_value__(None))) null
Lists can be of complex types, for instance
Input
:>>> class SomeInput(Input): ... a = int >>> SomeInputList = list_of(SomeInput) >>> SomeInputList([{'a': 123}]) [SomeInput(a=123)]
Variables may be given as constructor parameters:
>>> SomeInputList(Variable('lst')) $lst
Or already realized lists:
>>> SomeInputList(SomeInputList([{'a': 123}])) [SomeInput(a=123)]
- sgqlc.types.non_null(t)[source]¶
Generates non-null type (t!)
>>> class TypeWithNonNullFields(Type): ... a_int = non_null(int) ... a_float = non_null(Float) ... a_string = Field(non_null(String)) ... >>> TypeWithNonNullFields type TypeWithNonNullFields { aInt: Int! aFloat: Float! aString: String! }
Giving proper JSON data:
>>> json_data = {'aInt': 1, 'aFloat': 2.1, 'aString': 'hello'} >>> obj = TypeWithNonNullFields(json_data) >>> obj TypeWithNonNullFields(a_int=1, a_float=2.1, a_string='hello')
Giving incorrect JSON data:
>>> json_data = {'aInt': None, 'aFloat': 2.1, 'aString': 'hello'} >>> obj = TypeWithNonNullFields(json_data) Traceback (most recent call last): ... ValueError: TypeWithNonNullFields selection 'a_int': ... >>> json_data = {'aInt': 1, 'aFloat': None, 'aString': 'hello'} >>> obj = TypeWithNonNullFields(json_data) Traceback (most recent call last): ... ValueError: TypeWithNonNullFields selection 'a_float': ... >>> json_data = {'aInt': 1, 'aFloat': 2.1, 'aString': None} >>> obj = TypeWithNonNullFields(json_data) Traceback (most recent call last): ... ValueError: TypeWithNonNullFields selection 'a_string': ...
Note
Note that missing keys in JSON data are not considered
None
, and they won’t show initer(obj)
,__str__()
or__repr__()
>>> json_data = {'aInt': 1, 'aFloat': 2.1} >>> obj = TypeWithNonNullFields(json_data) >>> obj # repr() TypeWithNonNullFields(a_int=1, a_float=2.1) >>> for field_name in obj: ... print(field_name, repr(obj[field_name])) ... a_int 1 a_float 2.1