sgqlc.operation module¶
Generate Operations (Query and Mutations) using Python¶
Note
This module could be called “query”, however it should also generate
mutations and a class Query
could lead to mistakes, since the
users should define their own root Query
class with the
top-level queries in their GraphQL schema.
Users create instance of Operation
using the
Schema.Query
or Schema.Mutation
types. From there they proceed
accessing members, which will produce Selector
instances,
that once called will produce Selection
instances, which are
automatically added to a SelectionList
in the parent
(operation or selection). The following annotated GraphQL code helps
to understand the Python mapping:
query { # Operation
parent(arg: "value") { # Selector, called with arguments
child { # Selector, called without arguments
field # Selector called without arguments, Selection without alias
alias: field(other: 123)
}
sibling { x { y } }
}
}
op = Operation(Query)
parent = op.parent(arg='value')
child = parent.child
child.field()
child.field(other=123, __alias__='alias')
parent.sibling.x.y()
Operation
implements __str__()
and __repr__()
to
generate the GraphQL query for you. It also provide __bytes__()
to
produce compact output, without indentation. It can be passed to
sgqlc.endpoint.base.BaseEndpoint.__call__()
as is.
Another convenience is the __add__()
to apply the operation to a
resulting JSON data, interpreting the results and producing convenient
objects:
endpoint = HTTPEndpoint(url)
data = endpoint(op)
obj = op + data
print(obj.parent.child.field)
print(obj.parent.sibling.x.y)
Performance¶
When the endpoint is called passing Operation
it will
serialize the operation to a string. This may be costly depending on the
operation size and will be done on every endpoint usage, there is no
caching. Internally it does:
query = bytes(op).decode('utf-8')
Then it’s advised that those looking for extra performance to do this externally and pass the resulting string. A faster version of the code in the previous section is:
endpoint = HTTPEndpoint(url)
query = bytes(op).decode('utf-8')
data = endpoint(query) # faster if used multiple times
# The rest of the code is the same and uses 'op':
obj = op + data
print(obj.parent.child.field)
print(obj.parent.sibling.x.y)
This also hints at our second optimization: avoid creating operations using
arguments with values that changes. Replace those with the Variable
,
this will allow the query to be converted to string only once and will also
help the server – some of them employ caching.
# slower version
for a in args:
op = Operation(Query) # creates a new operation again
my_query = op.my_query(arg=a) # only thing that changed!
my_query.field() # do all the field processing again
data = endpoint(op) # serializes again
obj = op + data
process_my_query(obj)
# faster version
from sgqlc.types import Arg, String, Variable
op = Operation(Query, variables={
'a': Arg(String), # this must match the my_query arg type!
})
my_query = op.my_query(arg=Variable('a'))
my_query.field()
query = bytes(op).decode('utf-8')
for a in args:
data = endpoint(query, variables={'a': a}) # variables are plain JSON!
obj = op + data
process_my_query(obj)
Unfortunately SGQLC does not implements Automatic Persisted Queries yet, but that technique can be implemented on top of SGQLC. Contributions are welcome ;-)
Code Generator¶
If you are savvy enough to write GraphQL executable documents using their
Domain Specific Language (DSL) and already have schema.json
or access
to a server with introspection you may use the sgqlc-codegen operation
to
automatically generate the SGQLC Operations for you.
The generated code should be stable and can be committed to repositories,
leading to minimum diff
when updated.
- See examples:
GitHub defining a single parametrized (variables) query ListIssues and generates sample_operations.py.
Shopify uses shopify_operations.gql defining all the operations, including fragments and variables, and outputs the SGQLC code. See the generated shopify_operations.py.
Examples¶
Let’s start defining the types, including the schema root Query
:
>>> from sgqlc.types import *
>>> from datetime import datetime
>>> class Actor(Interface):
... login = non_null(str)
...
>>> class User(Type, Actor):
... name = str
...
>>> class Organization(Type, Actor):
... location = str
...
>>> class ActorConnection(Type):
... actors = Field(list_of(non_null(Actor)), args={'login': non_null(str)})
...
>>> class Assignee(Type):
... email = non_null(str)
...
>>> class UserOrAssignee(Union):
... __types__ = (User, Assignee)
...
>>> class Issue(Type):
... number = non_null(int)
... title = non_null(str)
... body = str
... reporter = non_null(User)
... assigned = UserOrAssignee
... commenters = ActorConnection
...
>>> class ReporterFilterInput(Input):
... name_contains = str
...
>>> class IssuesFilter(Input):
... reporter = list_of(ReporterFilterInput)
... start_date = non_null(datetime)
... end_date = datetime
...
>>> class Repository(Type):
... id = ID
... name = non_null(str)
... owner = non_null(Actor)
... issues = Field(list_of(non_null(Issue)), args={
... 'title_contains': str,
... 'reporter_login': str,
... 'filter': IssuesFilter,
... })
...
>>> class Query(Type):
... repository = Field(Repository, args={'id': non_null(ID)})
...
>>> class Mutation(Type):
... add_issue = Field(Issue, args={
... 'repository_id': non_null(ID),
... 'title': non_null(str),
... 'body': str,
... })
...
>>> global_schema
schema {
...
interface Actor {
login: String!
}
type User implements Actor {
login: String!
name: String
}
type Organization implements Actor {
login: String!
location: String
}
type ActorConnection {
actors(login: String!): [Actor!]
}
type Assignee {
email: String!
}
union UserOrAssignee = User | Assignee
type Issue {
number: Int!
title: String!
body: String
reporter: User!
assigned: UserOrAssignee
commenters: ActorConnection
}
input ReporterFilterInput {
nameContains: String
}
input IssuesFilter {
reporter: [ReporterFilterInput]
startDate: DateTime!
endDate: DateTime
}
type Repository {
id: ID
name: String!
owner: Actor!
issues(titleContains: String, reporterLogin: String, filter: IssuesFilter): [Issue!]
}
type Query {
repository(id: ID!): Repository
}
type Mutation {
addIssue(repositoryId: ID!, title: String!, body: String): Issue
}
}
Selecting to Generate Queries¶
Then let’s select numbers and titles of issues of repository with
identifier repo1
:
>>> op = Operation(Query)
>>> repository = op.repository(id='repo1')
>>> repository.issues.number()
number
>>> repository.issues.title()
title
>>> op # or repr(), prints out GraphQL!
query {
repository(id: "repo1") {
issues {
number
title
}
}
}
You can see we stored op.repository(id='repo1')
result in a
variable, later reusing it. Executing this statement will emit a new
Selection
and only one field selection is allowed in the
selection list (unless an alias is used). Trying the code below will
error:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.number() # ok!
number
>>> op.repository(id='repo1').issues.title() # fails
Traceback (most recent call last):
...
ValueError: repository already have a selection repository(id: "repo1") {
issues {
number
}
}. Maybe use __alias__ as param?
That is, if you wanted to query for two repositories, you should use
__alias__
argument in the call. But here would not produce the
query we want, as seen below:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.number()
number
>>> op.repository(id='repo1', __alias__='alias').issues.title()
title
>>> op # not what we want in this example, 2 independent queries
query {
repository(id: "repo1") {
issues {
number
}
}
alias: repository(id: "repo1") {
issues {
title
}
}
}
In our case, to get the correct query, do as in the first example and
save the result of op.repository(id='repo1')
.
Aliases may be used to rename fields everywhere, not just in the topmost query, and for other reasons other than allow two calls with the same name. One may use it to translate API fields to something else, example:
>>> op = Operation(Query)
>>> repository = op.repository(id='repo1')
>>> repository.issues.number(__alias__='code')
code: number
>>> op # or repr(), prints out GraphQL!
query {
repository(id: "repo1") {
issues {
code: number
}
}
}
Last but not least, in the first example you can also note that we’re
not calling issues
, just accessing its members. This is a shortcut
for an empty call, and the handle is saved for you (ease of use):
>>> op = Operation(Query)
>>> repository = op.repository(id='repo1')
>>> repository.issues().number()
number
>>> repository.issues().title()
title
>>> op # or repr(), prints out GraphQL!
query {
repository(id: "repo1") {
issues {
number
title
}
}
}
This could be rewritten saving the issues selector:
>>> op = Operation(Query)
>>> issues = op.repository(id='repo1').issues()
>>> issues.number()
number
>>> issues.title()
title
>>> op
query {
repository(id: "repo1") {
issues {
number
title
}
}
}
Or even simpler with __fields__(*names, **names_and_args)
:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.__fields__('number', 'title')
>>> op
query {
repository(id: "repo1") {
issues {
number
title
}
}
}
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.__fields__(
... number=True,
... title=True,
... )
>>> op
query {
repository(id: "repo1") {
issues {
number
title
}
}
}
Which also allows to include all but some fields:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.__fields__(
... __exclude__=('body', 'reporter', 'commenters'),
... )
>>> op
query {
repository(id: "repo1") {
issues {
number
title
assigned {
__typename
... on User {
login
name
}
... on Assignee {
email
}
}
}
}
}
Or using named arguments:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.__fields__(
... body=False,
... reporter=False,
... commenters=False,
... )
>>> op
query {
repository(id: "repo1") {
issues {
number
title
assigned {
__typename
... on User {
login
name
}
... on Assignee {
email
}
}
}
}
}
If no arguments are given to __fields__()
, then it defaults to
include every member, and this is done recursively:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.__fields__()
>>> op
query {
repository(id: "repo1") {
issues {
number
title
body
reporter {
login
name
}
assigned {
__typename
... on User {
login
name
}
... on Assignee {
email
}
}
commenters {
actors {
login
}
}
}
}
}
Named arguments may be used to provide fields with argument values:
>>> op = Operation(Query)
>>> op.repository(id='repo1').__fields__(
... issues={'title_contains': 'bug'}, # adds field and children
... )
>>> op
query {
repository(id: "repo1") {
issues(titleContains: "bug") {
number
title
body
reporter {
login
name
}
assigned {
__typename
}
}
}
}
Arguments can be given as tuple of key-value pairs as well:
>>> op = Operation(Query)
>>> op.repository(id='repo1').__fields__(
... issues=(('title_contains', 'bug'),), # adds field and children
... )
>>> op
query {
repository(id: "repo1") {
issues(titleContains: "bug") {
number
title
body
reporter {
login
name
}
assigned {
__typename
}
}
}
}
By default __typename
is only included when selecting Union
,
if that should be included in every Type
, then you must specify
__typename__
as a selected field. It’s handled recursively:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.__fields__('__typename__')
>>> op
query {
repository(id: "repo1") {
issues {
__typename
number
title
body
reporter {
__typename
login
name
}
assigned {
__typename
... on User {
login
name
}
... on Assignee {
email
}
}
commenters {
__typename
actors {
__typename
login
}
}
}
}
}
Or included using __typename__=True
:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.__fields__(__typename__=True)
>>> op
query {
repository(id: "repo1") {
issues {
__typename
number
title
body
reporter {
__typename
login
name
}
assigned {
__typename
... on User {
login
name
}
... on Assignee {
email
}
}
commenters {
__typename
actors {
__typename
login
}
}
}
}
}
If a field of a container type (interface or type) is used without explicit
fields as documented above, all of its fields will be added automatically.
It will avoid dependency loops and limit the allowed nest depth to 2 by
default, but that can be overridden with an explicit auto_select_depth
to __to_graphql__()
(which is used by str()
, repr()
and the
likes):
>>> op = Operation(Query)
>>> op.repository(id='repo1') # printed with depth=2 (default)
repository(id: "repo1") {
id
name
owner {
login
}
issues {
number
title
body
}
}
>>> op # the whole query printed with depth=2 (default)
query {
repository(id: "repo1") {
id
name
owner {
login
}
issues {
number
title
body
}
}
}
>>> print(op.__to_graphql__(auto_select_depth=1)) # omits owner/issues
query {
repository(id: "repo1") {
id
name
}
}
>>> print(op.__to_graphql__(auto_select_depth=3)) # shows reporter
query {
repository(id: "repo1") {
id
name
owner {
login
}
issues {
number
title
body
reporter {
login
name
}
assigned {
__typename
}
}
}
}
>>> print(op.__to_graphql__(auto_select_depth=4)) # shows assigned sub-types
query {
repository(id: "repo1") {
id
name
owner {
login
}
issues {
number
title
body
reporter {
login
name
}
assigned {
__typename
... on User {
login
name
}
... on Assignee {
email
}
}
commenters {
actors {
login
}
}
}
}
}
If __typename
is to be automatically selected, then use typename=True
:
>>> print(op.__to_graphql__(auto_select_depth=4, typename=True))
query {
repository(id: "repo1") {
__typename
id
name
owner {
__typename
login
}
issues {
__typename
number
title
body
reporter {
__typename
login
name
}
assigned {
__typename
... on User {
login
name
}
... on Assignee {
email
}
}
commenters {
__typename
actors {
__typename
login
}
}
}
}
}
Note
The built-in object type __typename
would cause issues with Python’s
name mangling as it would be translated to the private class member name.
In order to avoid this issue, whenever selecting GraphQL’s __typename
use the __typename__
Python name. Example:
>>> op = Operation(Query)
>>> op.repository(id='repo1').__typename__()
__typename
>>> op
query {
repository(id: "repo1") {
__typename
}
}
Interpret Query Results¶
Given the operation explained above:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.__fields__('number', 'title')
>>> op
query {
repository(id: "repo1") {
issues {
number
title
}
}
}
After calling the GraphQL endpoint, you should get a JSON object that matches the one below:
>>> json_data = {'data': {
... 'repository': {'issues': [
... {'number': 1, 'title': 'found a bug'},
... {'number': 2, 'title': 'a feature request'},
... ]},
... }}
To interpret this, simply add the data to the operation:
>>> obj = op + json_data
>>> repository = obj.repository
>>> for issue in repository.issues:
... print(issue)
Issue(number=1, title=found a bug)
Issue(number=2, title=a feature request)
Which are instances of classes declared in the beginning of example section:
>>> repository.__class__ is Repository
True
>>> repository.issues[0].__class__ is Issue
True
While it’s mostly the same as creating instances yourself:
>>> repository = Repository(json_data['data']['repository'])
>>> for issue in repository.issues:
... print(issue)
Issue(number=1, title=found a bug)
Issue(number=2, title=a feature request)
The difference is that it will handle aliases for you:
>>> op = Operation(Query)
>>> op.repository(id='repo1', __alias__='r_name1').issues.__fields__(
... number='code', title='headline',
... )
>>> op.repository(id='repo2', __alias__='r_name2').issues.__fields__(
... 'number', 'title',
... )
>>> op
query {
r_name1: repository(id: "repo1") {
issues {
code: number
headline: title
}
}
r_name2: repository(id: "repo2") {
issues {
number
title
}
}
}
>>> json_data = {'data': {
... 'r_name1': {'issues': [
... {'code': 1, 'headline': 'found a bug'},
... {'code': 2, 'headline': 'a feature request'},
... ]},
... 'r_name2': {'issues': [
... {'number': 10, 'title': 'something awesome'},
... {'number': 20, 'title': 'other thing broken'},
... ]},
... }}
>>> obj = op + json_data
>>> for issue in obj.r_name1.issues:
... print(issue)
Issue(code=1, headline=found a bug)
Issue(code=2, headline=a feature request)
>>> for issue in obj.r_name2.issues:
... print(issue)
Issue(number=10, title=something awesome)
Issue(number=20, title=other thing broken)
Updating also reflects on the correct backing store:
>>> obj.r_name2.name = 'repo2 name'
>>> json_data['data']['r_name2']['name']
'repo2 name'
It also works with auto selection:
>>> op = Operation(Query)
>>> op.repository(id='repo1')
repository(id: "repo1") {
id
name
owner {
login
}
issues {
number
title
body
}
}
>>> json_data = {'data': {
... 'repository': {'id': 'repo1', 'name': 'Repo #1'},
... }}
>>> obj = op + json_data
>>> obj.repository.name
'Repo #1'
And also if __typename__
is selected:
>>> op = Operation(Query)
>>> op.repository(id='repo1', __typename__=True)
repository(id: "repo1") {
__typename
id
name
owner {
__typename
login
}
issues {
__typename
number
title
body
}
}
>>> json_data = {'data': {
... 'repository': {
... '__typename': 'Repository', 'id': 'repo1', 'name': 'Repo #1',
... 'owner': {'__typename': 'Actor', 'login': 'name'},
... 'issues': [{
... '__typename': 'Issue', 'number': 1, 'title': 'title',
... }],
... },
... }}
>>> obj = op + json_data
>>> obj.repository
Repository(__typename__='Repository', id='repo1', name='Repo #1', owner=Actor(__typename__='Actor', login='name'), issues=[Issue(__typename__='Issue', number=1, title='title')])
Error Reporting¶
If the returned data contains only errors
and no data
,
the interpretation will raise an error GraphQLErrors
:
>>> json_data = {'errors': [{'message': 'some message'}]}
>>> try:
... obj = op + json_data
... except GraphQLErrors as ex:
... print('Got error:', repr(ex))
... print(ex.errors)
Got error: GraphQLErrors('some message'...
[{'message': 'some message'}]
If there are mixed data and errors, the object is returned with
__errors__
attribute set to the errors:
>>> json_data = {
... 'errors': [{'message': 'some message'}],
... 'data': {
... 'repository': {'id': 'repo1', 'name': 'Repo #1'},
... },
... }
>>> obj = op + json_data
>>> obj.repository.name
'Repo #1'
>>> obj.__errors__
[{'message': 'some message'}]
Mutations¶
Mutations are handled as well, just use that as Operation
root type:
>>> op = Operation(Mutation)
>>> op.add_issue(repository_id='repo1', title='an issue').__fields__()
>>> op
mutation {
addIssue(repositoryId: "repo1", title: "an issue") {
number
title
body
reporter {
login
name
}
assigned {
__typename
... on User {
login
name
}
... on Assignee {
email
}
}
commenters {
actors {
login
}
}
}
}
Inline Fragments & Interfaces¶
When a field specifies an interface such as the Repository.owner
in our example, only the interface fields can be queried. However,
the actual type may implement much more, and to solve that in GraphQL
we usually do an inline fragment ... on ActualType { field1, field2 }
.
To achieve that we use the __as__(ActualType)
on the selection list,
example:
>>> op = Operation(Query)
>>> repo = op.repository(id='repo1')
>>> repo.owner.login() # interface fields can be declared as usual
login
>>> repo.owner().__as__(Organization).location() # location field for Orgs
location
>>> repo.owner.__as__(User).name() # name field for Users
name
>>> repo.issues().assigned.__as__(Assignee).email()
email
>>> repo.issues().assigned.__as__(User).login()
login
>>> repo.issues().commenters().actors().login()
login
>>> repo.issues().commenters().actors().__as__(Organization).location()
location
>>> repo.issues().commenters().actors().__as__(User).name()
name
>>> op
query {
repository(id: "repo1") {
owner {
login
__typename
... on Organization {
location
}
... on User {
name
}
}
issues {
assigned {
__typename
... on Assignee {
email
}
... on User {
login
}
}
commenters {
actors {
login
__typename
... on Organization {
location
}
... on User {
name
}
}
}
}
}
}
Note that __typename
is automatically selected so it can create the
proper type when interprets the results:
>>> json_data = {'data': {'repository': {'owner': {
... '__typename': 'User',
... 'login': 'user',
... 'name': 'User Name',
... },
... 'issues': [
... {
... 'assigned': {'__typename': 'Assignee', 'email': 'e@mail.com'},
... 'commenters': {
... 'actors': [
... {'login': 'user', '__typename': 'User', 'name': 'User Name'},
... {'login': 'a-company', '__typename': 'Organization', 'location': 'that place'}
... ]
... }
... },
... {
... 'assigned': {'__typename': 'User', 'login': 'xpto'},
... 'commenters': {
... 'actors': [
... {'login': 'user', '__typename': 'User', 'name': 'User Name'},
... {'login': 'xpto', '__typename': 'User'}
... ]
... }
... },
... ],
... }}}
>>> obj = op + json_data
>>> obj.repository.owner
User(login='user', __typename__='User', name='User Name')
>>> for i in obj.repository.issues:
... print(i)
Issue(assigned=Assignee(__typename__=Assignee, email=e@mail.com), commenters=ActorConnection(actors=[User(login='user', __typename__='User', name='User Name'), Organization(login='a-company', __typename__='Organization', location='that place')]))
Issue(assigned=User(__typename__=User, login=xpto), commenters=ActorConnection(actors=[User(login='user', __typename__='User', name='User Name'), User(login='xpto', __typename__='User')]))
>>> json_data = {'data': {'repository': {'owner': {
... '__typename': 'Organization',
... 'login': 'a-company',
... 'name': 'A Company',
... }}}}
>>> obj = op + json_data
>>> obj.repository.owner
Organization(login='a-company', __typename__='Organization')
If the returned type doesn’t have an explicit type fields, the Interface field is returned:
>>> json_data = {'data': {'repository': {'owner': {
... '__typename': 'SomethingElse',
... 'login': 'something-else',
... 'field': 'value',
... }}}}
>>> obj = op + json_data
>>> obj.repository.owner
Actor(login='something-else', __typename__='SomethingElse')
In the unusual situation where __typename
is not returned,
it’s going to behave as the interface type as well:
>>> json_data = {'data': {'repository': {'owner': {
... 'login': 'user',
... 'name': 'User Name',
... }}}}
>>> obj = op + json_data
>>> obj.repository.owner
Actor(login='user')
Auto-selection works on inline fragments (casts) as well:
>>> op = Operation(Query)
>>> repo = op.repository(id='repo1')
>>> repo.owner.__as__(User).__fields__()
>>> op
query {
repository(id: "repo1") {
owner {
__typename
... on User {
login
name
}
}
}
}
>>> json_data = {'data': {'repository': {'owner': {
... '__typename': 'User', 'login': 'user', 'name': 'User Name',
... }}}}
>>> obj = op + json_data
>>> obj.repository.owner
User(__typename__='User', login='user', name='User Name')
Named Fragments¶
Named fragments are a way to reuse selection blocks and allow optimizations to be employed. They also allow shorter documents if the fragment is used more than once.
They are similar to Inline Fragments described above as they allow selecting on interfaces and unions.
>>> org_loc_frag = Fragment(Organization, 'OrganizationLocationFragment')
>>> org_loc_frag.location()
location
>>> org_login_frag = Fragment(Organization, 'OrganizationLoginFragment')
>>> org_login_frag.login()
login
>>> user_frag = Fragment(User, 'UserFragment')
>>> user_frag.name()
name
>>> assignee_frag = Fragment(Assignee, 'AssigneeFragment')
>>> assignee_frag.email()
email
>>> op = Operation(Query)
>>> repo = op.repository(id='repo1')
>>> repo.owner.login() # interface fields can be declared as usual
login
>>> repo.owner().__fragment__(org_loc_frag)
>>> repo.owner().__fragment__(org_login_frag) # can do many on the same type
>>> repo.owner.__fragment__(user_frag)
>>> repo.issues().assigned.__fragment__(assignee_frag)
>>> repo.issues().assigned.__fragment__(user_frag)
>>> repo.issues().commenters().actors().login()
login
>>> repo.issues().commenters().actors().__fragment__(org_loc_frag)
>>> repo.issues().commenters().actors().__fragment__(user_frag)
>>> op
query {
repository(id: "repo1") {
owner {
login
__typename
...OrganizationLocationFragment
...OrganizationLoginFragment
...UserFragment
}
issues {
assigned {
__typename
...AssigneeFragment
...UserFragment
}
commenters {
actors {
login
__typename
...OrganizationLocationFragment
...UserFragment
}
}
}
}
}
fragment OrganizationLocationFragment on Organization {
location
}
fragment OrganizationLoginFragment on Organization {
login
}
fragment UserFragment on User {
name
}
fragment AssigneeFragment on Assignee {
email
}
Note that __typename
is automatically selected so it can create the
proper type when interprets the results:
>>> json_data = {'data': {'repository': {'owner': {
... '__typename': 'User',
... 'login': 'user',
... 'name': 'User Name',
... },
... 'issues': [
... {
... 'assigned': {'__typename': 'Assignee', 'email': 'e@mail.com'},
... 'commenters': {
... 'actors': [
... {'login': 'user', '__typename': 'User', 'name': 'User Name'},
... {'login': 'a-company', '__typename': 'Organization', 'location': 'that place'}
... ]
... }
... },
... {
... 'assigned': {'__typename': 'User', 'name': 'User'},
... 'commenters': {
... 'actors': [
... {'login': 'user', '__typename': 'User', 'name': 'User Name'},
... {'login': 'xpto', '__typename': 'User'}
... ]
... }
... },
... ],
... }}}
>>> obj = op + json_data
>>> obj.repository.owner
User(login='user', __typename__='User', name='User Name')
>>> for i in obj.repository.issues:
... print(i)
Issue(assigned=Assignee(__typename__=Assignee, email=e@mail.com), commenters=ActorConnection(actors=[User(login='user', __typename__='User', name='User Name'), Organization(login='a-company', __typename__='Organization', location='that place')]))
Issue(assigned=User(__typename__=User, name=User), commenters=ActorConnection(actors=[User(login='user', __typename__='User', name='User Name'), User(login='xpto', __typename__='User')]))
>>> json_data = {'data': {'repository': {'owner': {
... '__typename': 'Organization',
... 'login': 'a-company',
... 'location': 'somewhere',
... 'name': 'A Company',
... }}}}
>>> obj = op + json_data
>>> obj.repository.owner
Organization(login='a-company', __typename__='Organization', location='somewhere')
If the returned type doesn’t have an explicit type fields, the Interface field is returned:
>>> json_data = {'data': {'repository': {'owner': {
... '__typename': 'SomethingElse',
... 'login': 'something-else',
... 'field': 'value',
... }}}}
>>> obj = op + json_data
>>> obj.repository.owner
Actor(login='something-else', __typename__='SomethingElse')
In the unusual situation where __typename
is not returned,
it’s going to behave as the interface type as well:
>>> json_data = {'data': {'repository': {'owner': {
... 'login': 'user',
... 'name': 'User Name',
... }}}}
>>> obj = op + json_data
>>> obj.repository.owner
Actor(login='user')
Auto-selection works on fragments as well:
>>> auto_sel_user = Fragment(User, 'AutoSelectedUser')
>>> auto_sel_user.__fields__()
>>> op = Operation(Query)
>>> op.repository(id='repo1').owner.__fragment__(auto_sel_user)
>>> op
query {
repository(id: "repo1") {
owner {
__typename
...AutoSelectedUser
}
}
}
fragment AutoSelectedUser on User {
login
name
}
>>> json_data = {'data': {'repository': {'owner': {
... '__typename': 'User', 'login': 'user', 'name': 'User Name',
... }}}}
>>> obj = op + json_data
>>> obj.repository.owner
User(__typename__='User', login='user', name='User Name')
Utilities¶
Starting with the first selection example:
>>> op = Operation(Query)
>>> repository = op.repository(id='repo1')
>>> repository.issues.number()
number
>>> repository.issues.title()
title
One can get a indented print out using repr()
:
>>> print(repr(op))
query {
repository(id: "repo1") {
issues {
number
title
}
}
}
>>> print(repr(repository))
repository(id: "repo1") {
issues {
number
title
}
}
>>> print(repr(repository.issues.number()))
number
Note that Selector
is different:
>>> print(repr(repository.issues.number))
Selector(field=number)
Or can get a compact print out without indentation using bytes()
:
>>> print(bytes(op).decode('utf-8'))
query {
repository(id: "repo1") {
issues {
number
title
}
}
}
>>> print(bytes(repository).decode('utf-8'))
repository(id: "repo1") {
issues {
number
title
}
}
>>> print(bytes(repository.issues.number()).decode('utf-8'))
number
Selection
and Selector
both implement len()
:
>>> len(op) # number of selections (here: top level)
1
>>> len(repository.issues()) # number of selections
2
>>> len(repository.issues) # number of selections (implicit empty call)
2
>>> len(repository.issues.title()) # leaf is always 1
1
Selection
and Selector
both implement dir()
to
also list fields:
>>> for name in dir(repository.issues()): # on selection also yields fields
... if not name.startswith('_'):
... print(name)
assigned
body
commenters
number
reporter
title
>>> for name in dir(repository.issues): # same for selector
... if not name.startswith('_'):
... print(name)
assigned
body
commenters
number
reporter
title
>>> for name in dir(repository.issues.number()): # no fields for scalar
... if not name.startswith('_'):
... print(name)
>>> for name in dir(repository.issues.number): # no fields for scalar
... if not name.startswith('_'):
... print(name)
Classes also implement iter()
to iterate over selections:
>>> for i, sel in enumerate(op):
... print('#%d: %s' % (i, sel))
#0: repository(id: "repo1") {
issues {
number
title
}
}
>>> for i, sel in enumerate(repository):
... print('#%d: %s' % (i, sel))
#0: issues {
number
title
}
>>> for i, sel in enumerate(repository.issues):
... print('#%d: %s' % (i, sel))
#0: number
#1: title
>>> for i, sel in enumerate(repository.issues()):
... print('#%d: %s' % (i, sel))
#0: number
#1: title
>>> for i, sel in enumerate(repository.issues.number()):
... print('#%d: %s' % (i, sel))
#0: number
Given a Selector
one can query a selection given its alias:
>>> op = Operation(Query)
>>> op.repository(id='repo1').issues.number()
number
>>> op.repository(id='repo2', __alias__='alias').issues.title()
title
>>> type(op['repository']) # it's the selector, not a selection!
<class 'sgqlc.operation.Selector'>
>>> op['repository'].__selection__() # default selection
repository(id: "repo1") {
issues {
number
}
}
>>> op['repository'].__selection__('alias') # aliased selection
alias: repository(id: "repo2") {
issues {
title
}
}
Which is useful to query the selection alias and arguments:
>>> op['repository'].__selection__('alias').__alias__
'alias'
>>> op['repository'].__selection__('alias').__args__
{'id': 'repo2'}
>>> op['repository'].__selection__().__args__
{'id': 'repo1'}
To get the arguments of the default (non-aliased) one can use the shortcut:
>>> op['repository'].__args__
{'id': 'repo1'}
- license:
ISC
- class sgqlc.operation.Operation(typ=None, name=None, **args)[source]¶
Bases:
object
GraphQL Operation: query or mutation.
The given type must be one of
schema.Query
orschema.Mutation
, defaults toglobal_schema.Query
or whatever is defined asglobal_schema.query_type
.The operation has an internal
sgqlc.operation.SelectionList
and will proxy attributes and item access to it, thus offering selectors and automatically handling selections:op = Operation() op.parent.field.child() op.parent.field(param1=value1, __alias__'q2').child()
Once data is fetched and parsed as JSON object containing the field
data
, the operation can be used to interpret this data using the addition operator (no clearly named method to avoid clashing with selections):op = Operation() op.parent.field.child() endpoint = HTTPEndpoint('http://my.server.com/graphql') json_data = endpoint(op) parent = op + json_data print(parent.field.child)
Example usage:
>>> op = Operation(global_schema.Query) >>> op.repository Selector(field=repository) >>> repository = op.repository(id='repo1') >>> repository.issues.number() number >>> repository.issues.title() title >>> op # or repr(), prints out GraphQL! query { repository(id: "repo1") { issues { number title } } }
The root type can be omitted, then
global_schema.Query
or whatever is defined as`global_schema.query_type
is used:>>> op = Operation() # same as Operation(global_schema.Query) >>> op.repository Selector(field=repository)
Operations can be named:
>>> op = Operation(name='MyOp') >>> repository = op.repository(id='repo1') >>> repository.issues.number() number >>> repository.issues.title() title >>> op # or repr(), prints out GraphQL! query MyOp { repository(id: "repo1") { issues { number title } } }
Operations can also have argument (variables), in this case it must be named (otherwise a name is created based on root type name, such as
"Query"
):>>> from sgqlc.types import Variable >>> op = Operation(repo_id=str, reporter_login=str) >>> repository = op.repository(id=Variable('repo_id')) >>> issues = repository.issues(reporter_login=Variable('reporter_login')) >>> issues.__fields__('number', 'title') >>> op # or repr(), prints out GraphQL! query Query($repoId: String, $reporterLogin: String) { repository(id: $repoId) { issues(reporterLogin: $reporterLogin) { number title } } }
If variable name conflicts with the parameter, you can pass them as a single
variables
parameter containing a dict.>>> from sgqlc.types import Variable >>> op = Operation(name='MyOperation', variables={'name': str}) >>> op.repository(id=Variable('name')).name() name >>> op # or repr(), prints out GraphQL! query MyOperation($name: String) { repository(id: $name) { name } }
Complex argument types are also supported as JSON object (GraphQL names and raw types) or actual types:
>>> op = Operation() >>> repository = op.repository(id='sgqlc') >>> issues = repository.issues(filter={ ... 'reporter': [{'nameContains': 'Gustavo'}], ... 'startDate': '2019-01-01T00:00:00+00:00', ... }) >>> issues.__fields__('number', 'title') >>> op # or repr(), prints out GraphQL! query { repository(id: "sgqlc") { issues(filter: {reporter: [{nameContains: "Gustavo"}], startDate: "2019-01-01T00:00:00+00:00"}) { number title } } }
>>> from datetime import datetime, timezone >>> from sgqlc.types import global_schema >>> op = Operation() >>> repository = op.repository(id='sgqlc') >>> issues = repository.issues(filter=global_schema.IssuesFilter( ... reporter=[global_schema.ReporterFilterInput(name_contains='Gustavo')], ... start_date=datetime(2019, 1, 1, tzinfo=timezone.utc), ... )) >>> issues.__fields__('number', 'title') >>> op # or repr(), prints out GraphQL! query { repository(id: "sgqlc") { issues(filter: {reporter: [{nameContains: "Gustavo"}], startDate: "2019-01-01T00:00:00+00:00"}) { number title } } }
Selectors can be acquired as attributes or items, but they must exist in the target type:
>>> op = Operation() >>> op.repository Selector(field=repository) >>> op['repository'] Selector(field=repository) >>> op.does_not_exist Traceback (most recent call last): ... AttributeError: query { } has no field does_not_exist >>> op['does_not_exist'] Traceback (most recent call last): ... KeyError: 'Query has no field does_not_exist'
- __weakref__¶
list of weak references to the object (if defined)
- class sgqlc.operation.Selection(alias, field, args, typename=None)[source]¶
Bases:
object
Select a field with in a container type.
Warning
Do not create instances directly, use
sgqlc.operation.Selector
instead.A selection matches the GraphQL statement to select a field from a type, it may contain an alias and parameters:
query { parent { field field(param1: value1, param2: value2) alias: field(param1: value1, param2: value2) } }
Attributes or items access will result in
sgqlc.operation.Selector
matching the target type field:parent.field.child
For container types one can provide a batch of fields using
sgqlc.operation.Selection.__fields__()
:# just field1 and field2 parent.field.child.__fields__('field1', 'field2') parent.field.child.__fields__(field1=True, field2=True) # field1 with parameters parent.field.child.__fields__(field1=dict(param1='value1')) # all but field2 parent.field.child.__fields__(field2=False) parent.field.child.__fields__(field2=None) parent.field.child.__fields__(__exclude__=('field2',))
If
__fields__()
is not explicitly called, then all fields are included. Note that this may lead to huge queries since it will result in recursive inclusion of all fields.Selectors will create selections when items or attributes are accessed, this is done by implicitly calling the selector with empty parameters.
However leafs (ie: scalars) must be explicitly called, otherwise they won’t generate a selection
# OK parent.field.child() # NOT OK: doesn't create a selection for child. parent.field.child
- __get_all_fields_selection_list(depth, trail, typename)¶
Create a new SelectionList, select all fields and return it
- class sgqlc.operation.SelectionList(typ)[source]¶
Bases:
object
List of
sgqlc.operation.Selection
in a type.Warning
Do not create instances directly, use
sgqlc.operation.Operation
instead.Create a selection list using a type to query its fields. Once fields are accessed, they will create
sgqlc.operation.Selector
object for that field, this allows to match the type structure, with easy to use API:parent.field.child() parent.field(param1=value1).child()
Direct usage example (not recommended):
>>> sl = SelectionList(global_schema.Repository) >>> sl += Selection('x', global_schema.Repository.id, {}) >>> sl # or repr() { x: id } >>> print(bytes(sl).decode('utf-8')) # no indentation { x: id } >>> sl.id # or any other field from Repository returns a Selector Selector(field=id) >>> sl['id'] # also as get item Selector(field=id) >>> sl.x # not the alias Traceback (most recent call last): ... AttributeError: { x: id } has no field x >>> sl['x'] # Traceback (most recent call last): ... KeyError: 'Repository has no field x' >>> sl.__type__ # returns the type the selection operates on type Repository { id: ID name: String! owner: Actor! issues(titleContains: String, reporterLogin: String, filter: IssuesFilter): [Issue!] }
- __as__(typ)[source]¶
Create a child selection list on the given type.
The selection list will be result in an inline fragment in the query with an additional query for
__typename
, which is later used to create the proper type when the results are interpreted.The newly created selection list is shared for all users of the same type in this selection list.
- __fields__(*names, **names_and_args)[source]¶
Select fields of a container type.
This is a helper to automate selection of fields of container types, such as giving a list of names to include, with or without parameters (passed as a mapping
name=args
).If no arguments are given, all fields are included.
If the keyword argument
__exclude__
is given a list of names, then all but those fields will be included. Alternatively one can exclude fields usingname=None
orname=False
as keyword argument.If a list of names is given as positional arguments, then only those names will be included. Alternatively one can include fields using
name=True
. To include fields with selection parameters, then usename=dict(...)
orname=list(...)
. To include fields without arguments and with aliases, use the shortcutname='alias'
.The special built-in field
__typename
is not selected by default. In order to select it, provide__typename__=True
as a parameter.# just field1 and field2 parent.field.child.__fields__('field1', 'field2') parent.field.child.__fields__(field1=True, field2=True) # field1 with parameters parent.field.child.__fields__(field1=dict(param1='value1')) # field1 renamed (aliased) to alias1 parent.field.child.__fields__(field1='alias1') # all but field2 parent.field.child.__fields__(field2=False) parent.field.child.__fields__(field2=None) parent.field.child.__fields__(__exclude__=('field2',)) # all and also include __typename parent.field.child.__fields__(__typename__=True) parent.field.child.__fields__('__typename__')
- class sgqlc.operation.Selector(parent, field)[source]¶
Bases:
object
Creates selection for a given field.
Warning
Do not create instances directly, use
sgqlc.operation.SelectionList
instead.Selectors are callable objects that will create
sgqlc.operation.Selection
entries in the parentsgqlc.operation.SelectionList
.Selectors will create selections when items or attributes are accessed, this is done by implicitly calling the selector with empty parameters.
However leafs (ie: scalars) must be explicitly called, otherwise they won’t generate a selection
# OK parent.field.child() # NOT OK: doesn't create a selection for child. parent.field.child
To select all fields from a container type, use
sgqlc.operation.Selection.__fields__()
, example:# just field1 and field2 parent.field.child.__fields__('field1', 'field2') parent.field.child.__fields__(field1=True, field2=True) # field1 with parameters parent.field.child.__fields__(field1=dict(param1='value1')) # all but field2 parent.field.child.__fields__(field2=False) parent.field.child.__fields__(field2=None) parent.field.child.__fields__(__exclude__=('field2',))
Note
GraphQL limits a single selection per type, as the field name is used in the return object. If you want to select the same field multiple times, like as using different parameters, then provide the
__alias__
parameter to the selector:# FAILS: parent.field.child(param1='value1') parent.field.child(param2='value2') # OK parent.field.child(param1='value1') parent.field.child(param2='value2', __alias__='child2')
- property __args__¶
Shortcut for self.__selection__().__args__
- __as__(typ)[source]¶
Create a selection list on the given type.
The selection list will be result in an inline fragment in the query with an additional query for
__typename
, which is later used to create the proper type when the results are interpreted.
- __call__(**args)[source]¶
Create a selection with the given parameters.
To provide an alias, use
__alias__
keyword argument.
- property __fields__¶
Calls the selector without arguments, creating a
Selection
instance and returnSelection.__fields__()
method, ready to be called.To query the actual field this selector operates, use
self.__field__