Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Road to 2.0 #500

Merged
merged 86 commits into from
Aug 27, 2017
Merged

[WIP] Road to 2.0 #500

merged 86 commits into from
Aug 27, 2017

Conversation

syrusakbary
Copy link
Member

@syrusakbary syrusakbary commented Jul 12, 2017

Main goals for Graphene 2.0:

  • Simpler Graphene types implementation (thanks to __init_subclass__, and a polyfill implementation for Python 2).
  • Simplified resolver API
  • Type Options from class arguments (only available in Python 3)

Test and provide feedback :)

You can start testing the features provided in this PR with pip install "graphene>=2.0.dev".
There are also available versions for each of the integrations:

  • Django: pip install "graphene-django>=2.0.dev"
  • SQLAlchemy: pip install "graphene-sqlalchemy>=2.0.dev"
  • Google App Engine: pip install "graphene-gae>=2.0.dev"

Please read the Upgrade Guide if your project was using previous versions of Graphene.
https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md


Breaking Change: Resolver API changed in 2.0

def resolve_field(self, info, **args):
    # In case needed, context = info.context
    return ...

Things to do before merging:

  • Main types
    • Simplified ObjectType implementation
    • Simplified Interface implementation
    • Simplified InputObjectType implementation
    • Simplified Union implementation
    • Simplified Mutation implementation
    • Simplified Scalar implementation
    • Simplified Enum implementation
  • Meta Options to use class arguments
  • Relay
    • Simplified ClientIdMutation implementation
    • Simplified Node implementation
    • Simplified Connection implementation
  • Automatic field resolver arguments from annotations
  • Abstract Types (Abstract ObjectType, Abstract Interface, ...)
  • Adapt external libraries depending on old code (ObjectTypeMeta)
  • Add mypy for static type checking
  • GraphQL-Core
    • Support custom InputObjectTypes object creation (other than dict).
  • Subscriptions (will be released directly on master).

Decided for next version:

  • Generic Types (with typing).
    • Field
    • Argument
    • InputField
    • List
    • NonNull
    • NodeField
    • Connection
  • Relay
  • Automatic fields from type annotations

New usage for Python 3

Meta Options using class arguments

Meta options can be created directly from class arguments instead of creating a Meta class in the type:

class Dog(ObjectType, interfaces=[Animal]):
    pass

Discarded for 2.0

While having field inference from object annotations is quite useful, it needs more effort to have it working it properly with mypy static typing. Because of that, the following will not be approached in the initial release of Graphene 2.0, but later.

Type inference

class Query(ObjectType):
    viewer_name: String
    all_users: List[User]

@matclayton
Copy link

This looks fantastic, we're currently running Graphene with a reasonable amount of traffic (100's queries per sec) site, and have noticed a significant amount (~50%) of CPU time / latency is spent doing query validation. When investigating fixes for this, persisted queries looks like the ideal scenario.

Are there any plans to add persisted queries to 2.0? or alternatively provide a hook to bypass validation/parsing if we know the query comes from a trusted source.

minor spelling and grammar changes UPGRADE-v2.0.md
@syrusakbary
Copy link
Member Author

syrusakbary commented Aug 7, 2017

@matclayton I think persisted queries is indeed quite a useful feature (and I think is a better strategy for saving time compared to bypass the validation step).

At the moment I'm more inclined to have an example of persited queries backed into one of the integrations, such as graphene-django as there is already a default way provided to persist the data using Django Models.

After that might be useful to put some of the common abstractions back into graphene so people can create it's own "persisted queries" implementations easily.

Thoughts?

@matclayton
Copy link

@syrusakbary That makes perfect sense and was sort of what I was expecting, I'd just forgotten the django specific code had been pulled out into a separate package.

@syrusakbary syrusakbary merged commit 0b92d3d into master Aug 27, 2017
@japrogramer
Copy link

@syrusakbary Hello, I know that subscriptions are going to be implemented in graphene soon but i just want to share how I managed to get them working with channels.

  • first I made a route for both http and ws
  1                                                                                                                                                                                                                                                                                                                                                         
4   channel_routing = [                                                                                                                                                                                                                                                                                                                                     
  1             route('http.request', ws_GQLData, path=r"^/gql"),                                                                                                                                                                                                                                                                                           
  2             route('websocket.connect', ws_GQL_connect, path=r"^/gql"),                                                                                                                                                                                                                                                                                  
  3             route('websocket.receive', ws_GQLData, path=r"^/gql"),                                                                                                                                                                                                                                                                                      
      ...                                                                                                                                                                                                                                                                                
  6         ]                                                                                                                                                                                                                                                                                                                                                      
  • than i accepted the connection
@allowed_hosts_only
def ws_GQL_connect(message):
    message.reply_channel.send({"accept": True})
 
  • parsed the request and executed the query
@channel_session                                                                                                                                                                                                                                                                                                                                            
def ws_GQLData(message):                                                                                                                                                                                                                                                                                                                                    
    clean = json.loads(message.content['text'])                                                                                                                                                                                                                                                                                                             
    try:                                                                                                                                                                                                                                                                                                                                                    
        query = clean['query']                                                                                                                                                                                                                                                                                                                              
    except:                                                                                                                                                                                                                                                                                                                                                 
        query = None                                                                                                                                                                                                                                                                                                                                        
    try:                                                                                                                                                                                                                                                                                                                                                    
        foovar = clean['variables']                                                                                                                                                                                                                                                                                                                         
    except:                                                                                                                                                                                                                                                                                                                                                 
        foovar = None                                                                                                                                                                                                                                                                                                                                       
    kwargs = {'context_value': message}                                                                                                                                                                                                                                                                                                                     
    result = schema.execute(query, variable_values=foovar,  **kwargs)                                                                                                                                                                                                                                                                                       
    message.reply_channel.send(                                                                                                                                                                                                                                                                                                                             
        {                                                                                                                                                                                                                                                                                                                                                   
            'text': str({'data': json.loads(json.dumps(result.data))})                                                                                                                                                                                                                                                                                      
        })          
  • Subscription class that adds user to Group
class ProductSubscritption(object):

    """test"""
    sub_product = graphene.Field(ProductType, description='subscribe to updated product', uuid=graphene.String())

    def resolve_sub_product(self, info, **args):
        uuid = args.get('uuid')
        qs = ProductType.get_queryset()
        try:
            Group('gqp.product-updated.{0}'.format(uuid)).add(info.context.reply_channel)
        except:
            pass
        return qs.get(uuid=uuid)
  • post_save receiver that sends messages, Note: here the client only receives the uuid that was updated,
    than it is up to the client to do another query to get the specific fields they want from that object if any

@receiver(post_save, sender=Product)
def send_update(sender, instance, created, *args, **kwargs):
    uuid = str(instance.uuid)
    if created:
        Group("gqp.product-add").send({'added': True})
        return
    Group('gqp.product-updated.{0}'.format(uuid))\
        .send({'text': uuid})

  • than finally the js
socket = new WebSocket("ws://" + window.location.host + "/gql");
socket.onmessage = function(e) {
    alert(e.data);
}
socket.send(JSON.stringify({query: 'subscription {\
  subProduct (uuid: "714a3f4f-4a21-4ff9-b058-f12ad6389f72"){\
    id,\
    disabled,\
    title,\
  }\
}'}))

@AgentChris
Copy link

AgentChris commented Dec 8, 2017

what does ProductType represents?, or can you send my a link with the file, to see it better

@japrogramer
Copy link

japrogramer commented Dec 9, 2017

@AgentChris ProductType is the DjangoObjectType
mine looks like this, /* dont worry about TypeMixin or the resolver */

 25 class ProductType(TypeMixin, DjangoObjectType):
 26     attr = 'uuid'
 27
 28     """Query API def for Product"""
 29
 30     class Meta:
 31         model = Product
 32         only_fields = ('uuid', 'owner', 'title', 'active', 'disabled', 'description')
 33
 34         interfaces = (relay.Node, )
 35
 36     def resolve_id(self, info, **args):
 37 
           return str(self.uuid)

@AgentChris
Copy link

this works, but when i use react-apollo, it doesn't seems to work

@japrogramer
Copy link

japrogramer commented Dec 15, 2017

@AgentChris are you spliting the link so that subscriptions get routed to ws?
And what are the errors you are getting ?
The way I am planning on using the subscription is every time I get an update on that channel I invalidate and schedule a different query, thus causing the data to be re-fetch and my component to update.

@japrogramer
Copy link

@AgentChris Oh, also you might want to look at the actual post data send by apollo,
Im sure that the lines like query = clean['query'] might not be working because of that .. im in the process of implementing subscriptions with apollo .. will have answer in a while

@japrogramer
Copy link

@AgentChris Im currently trying to get apollo to connect to my django implementation of subscriptions if you want to track my progress, I ran into an issue here
apollographql/subscriptions-transport-ws#319

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants