Giter VIP home page Giter VIP logo

graphql-relay-py's Introduction

Relay Library for GraphQL Python

GraphQL-relay-py is the Relay library for GraphQL-core.

It allows the easy creation of Relay-compliant servers using GraphQL-core.

GraphQL-Relay-Py is a Python port of graphql-relay-js, while GraphQL-Core is a Python port of GraphQL.js, the reference implementation of GraphQL for JavaScript.

Since version 3, GraphQL-Relay-Py and GraphQL-Core support Python 3.6 and above only. For older versions of Python, you can use version 2 of these libraries.

PyPI version Test Status Lint Status Code Style

Getting Started

A basic understanding of GraphQL and of the GraphQL Python implementation is needed to provide context for this library.

An overview of GraphQL in general is available in the README for the Specification for GraphQL.

This library is designed to work with the the GraphQL-Core Python reference implementation of a GraphQL server.

An overview of the functionality that a Relay-compliant GraphQL server should provide is in the GraphQL Relay Specification on the Relay website. That overview describes a simple set of examples that exist as tests in this repository. A good way to get started with this repository is to walk through that documentation and the corresponding tests in this library together.

Using Relay Library for GraphQL Python (graphql-core)

Install Relay Library for GraphQL Python

pip install graphql-core
pip install graphql-relay

When building a schema for GraphQL, the provided library functions can be used to simplify the creation of Relay patterns.

All the functions that are explained in the following sections must be imported from the top level of the graphql_relay package, like this:

from graphql_relay import connection_definitions

Connections

Helper functions are provided for both building the GraphQL types for connections and for implementing the resolve method for fields returning those types.

  • connection_args returns the arguments that fields should provide when they return a connection type that supports bidirectional pagination.
  • forward_connection_args returns the arguments that fields should provide when they return a connection type that only supports forward pagination.
  • backward_connection_args returns the arguments that fields should provide when they return a connection type that only supports backward pagination.
  • connection_definitions returns a connection_type and its associated edgeType, given a name and a node type.
  • connection_from_array is a helper method that takes an array and the arguments from connection_args, does pagination and filtering, and returns an object in the shape expected by a connection_type's resolve function.
  • cursor_for_object_in_connection is a helper method that takes an array and a member object, and returns a cursor for use in the mutation payload.
  • offset_to_cursor takes the index of a member object in an array and returns an opaque cursor for use in the mutation payload.
  • cursor_to_offset takes an opaque cursor (created with offset_to_cursor) and returns the corresponding array index.

An example usage of these methods from the test schema:

ship_edge, ship_connection = connection_definitions(ship_type, "Ship")

faction_type = GraphQLObjectType(
    name="Faction",
    description="A faction in the Star Wars saga",
    fields=lambda: {
        "id": global_id_field("Faction"),
        "name": GraphQLField(GraphQLString, description="The name of the faction."),
        "ships": GraphQLField(
            ship_connection,
            description="The ships used by the faction.",
            args=connection_args,
            resolve=lambda faction, _info, **args: connection_from_array(
                [get_ship(ship) for ship in faction.ships], args
            ),
        ),
    },
    interfaces=[node_interface],
)

This shows adding a ships field to the Faction object that is a connection. It uses connection_definitions(ship_type, "Ship") to create the connection type, adds connection_args as arguments on this function, and then implements the resolver function by passing the array of ships and the arguments to connection_from_array.

Object Identification

Helper functions are provided for both building the GraphQL types for nodes and for implementing global IDs around local IDs.

  • node_definitions returns the Node interface that objects can implement, and returns the node root field to include on the query type. To implement this, it takes a function to resolve an ID to an object, and to determine the type of a given object.
  • to_global_id takes a type name and an ID specific to that type name, and returns a "global ID" that is unique among all types.
  • from_global_id takes the "global ID" created by to_global_id, and returns the type name and ID used to create it.
  • global_id_field creates the configuration for an id field on a node.
  • plural_identifying_root_field creates a field that accepts a list of non-ID identifiers (like a username) and maps then to their corresponding objects.

An example usage of these methods from the test schema:

def get_node(global_id, _info):
    type_, id_ = from_global_id(global_id)
    if type_ == "Faction":
        return get_faction(id_)
    if type_ == "Ship":
        return get_ship(id_)
    return None  # pragma: no cover

def get_node_type(obj, _info, _type):
    if isinstance(obj, Faction):
        return faction_type.name
    return ship_type.name

node_interface, node_field = node_definitions(get_node, get_node_type)[:2]

faction_type = GraphQLObjectType(
    name="Faction",
    description="A faction in the Star Wars saga",
    fields=lambda: {
        "id": global_id_field("Faction"),
        "name": GraphQLField(GraphQLString, description="The name of the faction."),
        "ships": GraphQLField(
            ship_connection,
            description="The ships used by the faction.",
            args=connection_args,
            resolve=lambda faction, _info, **args: connection_from_array(
                [get_ship(ship) for ship in faction.ships], args
            ),
        ),
    },
    interfaces=[node_interface],
)

query_type = GraphQLObjectType(
    name="Query",
    fields=lambda: {
        "rebels": GraphQLField(faction_type, resolve=lambda _obj, _info: get_rebels()),
        "empire": GraphQLField(faction_type, resolve=lambda _obj, _info: get_empire()),
        "node": node_field,
    },
)

This uses node_definitions to construct the Node interface and the node field; it uses from_global_id to resolve the IDs passed in the implementation of the function mapping ID to object. It then uses the global_id_field method to create the id field on Faction, which also ensures implements the node_interface. Finally, it adds the node field to the query type, using the node_field returned by node_definitions.

Mutations

A helper function is provided for building mutations with single inputs and client mutation IDs.

  • mutation_with_client_mutation_id takes a name, input fields, output fields, and a mutation method to map from the input fields to the output fields, performing the mutation along the way. It then creates and returns a field configuration that can be used as a top-level field on the mutation type.

An example usage of these methods from the test schema:

class IntroduceShipMutation:

    def __init__(self, shipId, factionId, clientMutationId=None):
        self.shipId = shipId
        self.factionId = factionId
        self.clientMutationId = clientMutationId

def mutate_and_get_payload(_info, shipName, factionId, **_input):
    new_ship = create_ship(shipName, factionId)
    return IntroduceShipMutation(shipId=new_ship.id, factionId=factionId)

ship_mutation = mutation_with_client_mutation_id(
    "IntroduceShip",
    input_fields={
        "shipName": GraphQLInputField(GraphQLNonNull(GraphQLString)),
        "factionId": GraphQLInputField(GraphQLNonNull(GraphQLID)),
    },
    output_fields={
        "ship": GraphQLField(
            ship_type, resolve=lambda payload, _info: get_ship(payload.shipId)
        ),
        "faction": GraphQLField(
            faction_type, resolve=lambda payload, _info: get_faction(payload.factionId)
        ),
    },
    mutate_and_get_payload=mutate_and_get_payload,
)

mutation_type = GraphQLObjectType(
    "Mutation", fields=lambda: {"introduceShip": ship_mutation}
)

This code creates a mutation named IntroduceShip, which takes a faction ID and a ship name as input. It outputs the Faction and the Ship in question. mutate_and_get_payload then gets each input field as keyword parameter, performs the mutation by constructing the new ship, then returns an object that will be resolved by the output fields.

Our mutation type then creates the introduceShip field using the return value of mutation_with_client_mutation_id.

Contributing

After cloning this repository from GitHub, we recommend using Poetry to create a test environment. With poetry installed, you do this with the following command:

poetry install

You can then run the complete test suite like this:

poetry run pytest

In order to run only a part of the tests with increased verbosity, you can add pytest options, like this:

poetry run pytest tests/node -vv

In order to check the code style with flake8, use this:

poetry run flake8

Use the tox command to run the test suite with different Python versions and perform all additional source code checks. You can also restrict tox to an individual environment, like this:

poetry run tox -e py39

graphql-relay-py's People

Contributors

cito avatar jhgg avatar jkimbo avatar markedwards avatar mvanlonden avatar pmlandwehr avatar syrusakbary avatar vladr11 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-relay-py's Issues

Six package dependencies incompatible with awsebcli

Hello

I'm trying to use awsebcli to deploy my web application to AWS. When I try to install awsebcli I get next error:

Warning: Your dependencies could not be resolved. You likely have a mismatch in your sub-dependencies.
First try clearing your dependency cache with $ pipenv lock --clear, then try the original command again.
Alternatively, you can use $ pipenv install --skip-lock to bypass this mechanism, then run $ pipenv graph to inspect the situation.
Hint: try $ pipenv lock --pre if it is a pre-release dependency.
Could not find a version that matches six<1.12.0,<2,>=1.10.0,>=1.11.0,>=1.12,>=1.3.0,>=1.4.0,>=1.5,>=1.9.0
Tried: 0.9.0, 0.9.1, 0.9.2, 1.0.0, 1.1.0, 1.2.0, 1.3.0, 1.4.0, 1.4.1, 1.5.0, 1.5.0, 1.5.1, 1.5.1, 1.5.2, 1.5.2, 1.6.0, 1.6.0, 1.6.1, 1.6.1, 1.7.0, 1.7.0, 1.7.1, 1.7.1, 1.7.2, 1.7.2, 1.7.3, 1.7.3, 1.8.0, 1.8.0, 1.9.0, 1.9.0, 1.10.0, 1.10.0, 1.11.0, 1.11.0, 1.12.0, 1.12.0, 1.13.0, 1.13.0, 1.14.0, 1.14.0, 1.15.0, 1.15.0
Skipped pre-versions: 1.0b1
There are incompatible versions in the resolved dependencies.

I'm using pipenv. graphql-relay-py requires six>=1.12 and awsebcli requires >=1.11.0,<1.12.0

is there any way to resolve this dependency?

Unicode cursors passed to get_offset_with_default are rejected

I'm unsure if this is an actual problem or intentional on graphql-relay's end but when using graphene hooked into django (1.8.6) cursor based querying returns the same result set as without. This is due to the aforementioned function (in graphql_relay.connection.arrayconnection) specifically only allowing instances of str. I monkeypatched an alternative function that casts the unicode to str and was able to get sliced results.

has_previous_page and has_next_page always False while navigating

While using the pageInfo hasPreviousPage and hasNextPage cursor based navigation (before and after), it seems these results are always set to False.

The corresponding code (https://github.com/graphql-python/graphql-relay-py/blob/master/graphql_relay/connection/arrayconnection.py#L104-L105):

return connection_type(
    edges=edges,
    page_info=pageinfo_type(
        start_cursor=first_edge_cursor,
        end_cursor=last_edge_cursor,
        has_previous_page=isinstance(last, int) and start_offset > lower_bound,
        has_next_page=isinstance(first, int) and end_offset < upper_bound
    )
)

Querying with first: 5, after: "xxx" in this case always results in hasPreviousPage: false.
The same for last: 5, before: "xxx", which results always in hasNextPage: false.

Are both poetry and setuptools necessary?

I am working on improving how this package is bundled in nixpkgs and notice that both poetry and setuptools are declared as build dependencies in pyproject.toml. Also, there is a setup.cfg and setup.py file. Are both of these build tools necessary?

Error using graphql-relay with django

I try to add graphene to my project and I have this error

python manage.py
Traceback (most recent call last):
  File "manage.py", line 17, in <module>
    execute_from_command_line(sys.argv)
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 395, in execute
    django.setup()
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/django/apps/registry.py", line 91, in populate
    app_config = AppConfig.create(entry)
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/django/apps/config.py", line 224, in create
    import_module(entry)
  File "/usr/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 848, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/graphene_django/__init__.py", line 1, in <module>
    from .fields import DjangoConnectionField, DjangoListField
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/graphene_django/fields.py", line 5, in <module>
    from graphql_relay.connection.arrayconnection import (
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/graphql_relay/connection/arrayconnection.py", line 1, in <module>
    from ..utils import base64, unbase64, is_str
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/graphql_relay/__init__.py", line 1, in <module>
    from .connection.connection import (
  File "/home/abusquets/devel/backend/.venv/lib/python3.8/site-packages/graphql_relay/connection/connection.py", line 3, in <module>
    from graphql.type import (
ModuleNotFoundError: No module named 'graphql.type'

My packages are:
graphene = "2.1.9"
graphene-django = "2.15.0"
graphql-core = "2.3.2"
graphql-relay = "2.0.1"

Problem with graphql_relay.connection.arrayconnection

Hello.
After 3.1.1 update graphql_relay.connection.arrayconnection renamed to graphql_relay.connection.array_connection and some packages not working.
Can you add graphql_relay.connection.arrayconnection with import * for reverse compatibility?

Why are nodes optional?

I was trying to dig through this to figure it out, and I apologize if there's a good reason for this.

It seems to me that a paginated connection (e.g. DjangoConnectionField or DjangoFilterConnectionField) always has edges and nodes. What's the use case for these being nullable?

The reason I bring this up is because, when using TypeScript with the generated schema, it results in lengthy checks like this:

            {edges.map(edge => (
              <div>
                {edge &&
                  edge.node &&
                  edge.node.id (
                    ...<JSX here>

As far as I can tell this is set up here:
https://github.com/graphql-python/graphql-relay-py/blob/master/graphql_relay/connection/connection.py#L44

Could there be required on edges/nodes? At least as an option it'd be a breaking change. Right now I'm not sure how to override this.

Incude tests in Python package?

Hi there,

I'm building an RPM package for graphql-relay at https://build.opensuse.org, but it would be great, if the unittests were part of the actual Python package on PyPI, so that I could run them after the build process to verify the correct installation.

Would you mind including them in the next release?

from_global_id() differs from reference implementation

The python from_global_id does not exhibit the same behavior as the reference TypeScript implementation when handling invalid input.

Examples:

  • from_global_id("invalid") raises binascii.Error
  • from_global_id("") raises TypeError
  • from_global_id(b64encode(b"foo")) raises TypeError

In the first two cases, the reference implementation returns {type: "", id: ""}. In the third case, it returns {type: "", id: "foo"}. In no case does it throw an error.

This might seem like nitpicking, but it can cause significant differences in how user input ends up being handled.

Return a tuple instead of ResolvedGlobalId

I think we can remove the need for a class to hold return values given that we now are dealing with Python and have the ability to unpack. I'm also not entirely sure why it was in the js lib as es6 has unpacking ("destructuring").

Global IDs are not URL safe

Currently, global IDs are generated using the full Base64 charset. This includes the characters + and /, which are not safe for displaying in URLs.

The implementation the base64 utilities found here should use urlsafe_b64encode and urlsafe_b64decode respectively.

How can I get offset from cursor using exposed function?

I found cursor_to_offset function internally used.
I know that I can define cursor myself, but I think there has to be a way to deal with predefined cursor. But there are not exposed.
And I'm using graphene actually, not graphql_relay directly. I think that they both don't offer such way neither.

Paginate without requiring length

The current implementation requires you to pass the total length of the iterable, and this usually means making an extra query to find out the count. There's no need for this, since we can actually accomplish the same results by trying to fetch n+1 results, which will mean that there are more to be consumed, without the need for the real total count.

This is the custom implementation we have applied to spare us the count query, which can be costly and adds an unnecessary overhead to every query. We only need the real total count, when that value is requested (using a custom CountableConnection)

class CountableConnection(graphene.relay.Connection):
    class Meta:
        abstract = True

    total_count = graphene.Int()

    @staticmethod
    def resolve_total_count(root, info, *args, **kwargs):
        try:
            t_count = root.iterable.count()
        except:
            t_count = len(root.iterable)
        
        return t_count

def connection_from_list_slice(list_slice, args=None, connection_type=None,
                               edge_type=None, pageinfo_type=None):
    '''
    Given an iterator it consumes the needed amount based on the pagination
    params, and also tries to figure out if there are more results to be
    consumed. We do so by trying to fetch one more element than the specified
    amount, if we were able to fetch n+1 it means there are more to be consumed.

    This spares the caller passing the total count of results, which
    usually means making an extra query just to find out that number.
    '''
    from graphql_relay.utils import base64, unbase64, is_str
    from graphql_relay.connection.connectiontypes import Connection, PageInfo, Edge

    connection_type = connection_type or Connection
    edge_type = edge_type or Edge
    pageinfo_type = pageinfo_type or PageInfo

    args = args or {}

    before = args.get('before')
    after = args.get('after')
    first = args.get('first')
    last = args.get('last')

    if first:
        after = get_offset_with_default(after, -1) + 1
        _slice = list_slice[after:  max(after, 0) + first + 1]  # fetch n+1

        items = _slice[:-1]
        if len(items) < first:
            items = _slice[:]  # If there are not enough, get them all

        edges = [
            edge_type(
                node=node,
                cursor=offset_to_cursor(after + i)
            )
            for i, node in enumerate(items)
        ]

    elif last:
        if before:
            before = get_offset_with_default(before)
            _slice = list_slice[max(before-last-1, 0):before]  # fetch n+1

        else:
            # beware that not specifying before results in the need
            # to calculate the total amount
            _slice = list_slice[(last*-1)-1:]

            try:  
                before = list_slice.count()
            except:
                before = len(list_slice)

        items = _slice[1:]
        if len(items) < last:
            items = _slice[:]  # If there are not enough, get them all

        edges = [
            edge_type(
                node=node,
                cursor=offset_to_cursor(before - last -1 + i)
            )
            for i, node in enumerate(items)
        ]

    else:  # we are not allowing to pass after/before without first/last
        items = list_slice[:]
        edges = [
            edge_type(
                node=node,
                cursor=offset_to_cursor(i)
            )
            for i, node in enumerate(items)
        ]

    first_edge_cursor = edges[0].cursor if edges else None
    last_edge_cursor = edges[-1].cursor if edges else None
    
    has_previous_page = False
    if (isinstance(last, int) and len(_slice) > last) or after > 0:
        has_previous_page = True

    return connection_type(
        edges=edges,
        page_info=pageinfo_type(
            start_cursor=first_edge_cursor,
            end_cursor=last_edge_cursor,
            has_previous_page=has_previous_page,
            has_next_page=len(_slice) > first if isinstance(first, int) else False
        )
    )

@syrusakbary Not sure if we want this as the default or at least have this as an optional stragey.
If this sounds reasonable I can make a PR for this.

Type checking fails when using constructor overrides in arrayconnection.py

Overriding one of the type constructors in one of the resolvers in arrayconnection.py breaks the return type of resolver.

For instance:

@dataclass
class CustomConnection:
    edges: List[Edge]
    pageInfo: PageInfo

# connection is typed as Connection, but it is actually CustomConnection
connection = connection_from_array(
    [], 
    connection_type=CustomConnection,
)

As with issue #29, this can be cleanly resolved using custom Protocol definitions, although there is the downside noted in that issue of the additional typing-extensions dependency.

Unlike the other issue, I think you could implement a partial solution using Callable, TypeVar, and overload from typing, without having the Protocol dependency. I'm pretty sure the resulting code would be extremely difficult to understand though, and it would be very verbose.

[Question] How can I customize the name of the field id is retrieve by?

Im trying to have relay node compose the global id in a custom way for a specific type. Instead of using the id attribute of my object I want it to use the uuid attribute. I know that i can create a custom node class to customize how the id is encoded and decoded: http://docs.graphene-python.org/en/latest/relay/nodes/
but what i want to do Is also customize the name of the field the id is gotten from ... this code suggest that it can be done by passing id_fetcher but i Dont know where or how this function is called https://github.com/graphql-python/graphql-relay-py/blob/master/graphql_relay/node/node.py#L70

PEP8 issues

Running flake8 on the source reveals a variety of issues.

More importantly though is the JavaScript naming convention being used (not reported by flake8). Please use the recommended naming convention as specified in PEP8.

v3.1.3 is incompatible with graphql-core v3.1.x

Using [email protected] and [email protected] gives some import errors:

2022-01-22 14:12:02,961 P3528 [INFO]        from graphene import UUID, Enum
2022-01-22 14:12:02,961 P3528 [INFO]      File "/var/app/venv/staging-LQM1lest/lib/python3.7/site-packages/graphene/__init__.py", line 2, in <module>
2022-01-22 14:12:02,961 P3528 [INFO]        from .relay import (
2022-01-22 14:12:02,961 P3528 [INFO]      File "/var/app/venv/staging-LQM1lest/lib/python3.7/site-packages/graphene/relay/__init__.py", line 1, in <module
>
2022-01-22 14:12:02,961 P3528 [INFO]        from .node import Node, is_node, GlobalID
2022-01-22 14:12:02,961 P3528 [INFO]      File "/var/app/venv/staging-LQM1lest/lib/python3.7/site-packages/graphene/relay/node.py", line 4, in <module>
2022-01-22 14:12:02,962 P3528 [INFO]        from graphql_relay import from_global_id, to_global_id
2022-01-22 14:12:02,962 P3528 [INFO]      File "/var/app/venv/staging-LQM1lest/lib/python3.7/site-packages/graphql_relay/__init__.py", line 7, in <module>
2022-01-22 14:12:02,962 P3528 [INFO]        from .connection.connection import (
2022-01-22 14:12:02,962 P3528 [INFO]      File "/var/app/venv/staging-LQM1lest/lib/python3.7/site-packages/graphql_relay/connection/connection.py", line 3
, in <module>
2022-01-22 14:12:02,962 P3528 [INFO]        from graphql import (
2022-01-22 14:12:02,962 P3528 [INFO]    ImportError: cannot import name 'GraphQLNamedOutputType' from 'graphql' (/var/app/venv/staging-LQM1lest/lib/python
3.7/site-packages/graphql/__init__.py)

This commit added the line that's failing:
bb5f935#diff-b895c3c5263fde224cc02c9d85e98cb6e094c323260e8ea7900db89df72a17a6R13

The release notes say that it should be "compatible with graphql-core version 3.1.".

It seems that's not the case. The exporting of happened in graphql-python/graphql-core@5b9a728, which seems to be only available in 3.2.

For now I've worked around this by locking to an earlier version of graphql-relay-py and I'm not sure of the exact approach you'd want to use for fixing the problem. Perhaps changing install_requires in setup.py?

Incompatibility with new graphql-core resolvers API

Hi! The tests are now failing - see a Travis build from my just-forked repository: https://travis-ci.org/messa/graphql-relay-py/builds/485088298

GraphQLLocatedError("<lambda>() got an unexpected keyword argument 'first'",)

I think the major cause of the errors is the API change in graphql-core v2.0.0:

New resolvers API. Moving from resolver(root, args, context, info) to resolver(root, info, **args)

It would be possible to limit versions of the graphql-core depency below 2.0.0 (it's now 'graphql-core>=0.5.0'), but I think that would be not very nice.

Would you accept a pull request that updates the code to the new resolver API?

Typing of array_slice as Sequence in connection methods

I'm implementing graphql_relay in a GraphQL implementation to handle Connection responses by passing django.db.models.QuerySet objects to the array_slice argument of connection_from_array_slice(), much in the same way that Graphene does.

Although I have assured myself that this works, the typing of array_slice as Sequence is a bit disconcerting, since QuerySet does not qualify as a Sequence instance. As far as I can tell, there isn't an appropriate standard type that would solve this, however. The best idea I have would be to define a custom Protocol, but that might pose compatibility challenges for older python installs.

Is there any appetite to resolve this so that a class like QuerySet type-checks without having to cast or ignore it? Barring that, can it at least be stated that graphql_relay will continue to support QuerySet in this way? I assume that is the intention since Graphene relies on it.

Test errors with graphql-core 3.2.0

After updating graphql-core to 3.2.0 we're seeing the following tests fail on graphql-relay 3.1.0:

=================================== FAILURES ===================================
_______________________ test_correctly_refetches_rebels ________________________

    @mark.asyncio
    async def test_correctly_refetches_rebels():
        query = """
          query RebelsRefetchQuery {
            node(id: "RmFjdGlvbjox") {
              id
              ... on Faction {
                name
              }
            }
          }
        """
        expected = {
            "node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
        }
        result = await graphql(StarWarsSchema, query)
>       assert result == (expected, None)
E       AssertionError: assert ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])]) == ({'node': {'id': 'RmFjdGlvbjox', 'name': 'Alliance to Restore the Republic'}},\n None)
E         +ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])])
E         -({'node': {'id': 'RmFjdGlvbjox', 'name': 'Alliance to Restore the Republic'}}, None)
E         Full diff:
E         + ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])],
E         - (
E         -  {'node': {'id': 'RmFjdGlvbjox',
E         -            'name': 'Alliance to Restore the Republic'}},
E         -  None,
E           )

tests/test_star_wars_object_identification.py:41: AssertionError
_______________________ test_correctly_refetches_empire ________________________

    @mark.asyncio
    async def test_correctly_refetches_empire():
        query = """
          query EmpireRefetchQuery {
            node(id: "RmFjdGlvbjoy") {
              id
              ... on Faction {
                name
              }
            }
          }
        """
        expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
        result = await graphql(StarWarsSchema, query)
>       assert result == (expected, None)
E       AssertionError: assert ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])]) == ({'node': {'id': 'RmFjdGlvbjoy', 'name': 'Galactic Empire'}}, None)
E         +ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])])
E         -({'node': {'id': 'RmFjdGlvbjoy', 'name': 'Galactic Empire'}}, None)
E         Full diff:
E         - ({'node': {'id': 'RmFjdGlvbjoy', 'name': 'Galactic Empire'}}, None)
E         + ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])])

tests/test_star_wars_object_identification.py:73: AssertionError
________________________ test_correctly_refetches_xwing ________________________

    @mark.asyncio
    async def test_correctly_refetches_xwing():
        query = """
          query XWingRefetchQuery {
            node(id: "U2hpcDox") {
              id
              ... on Ship {
                name
              }
            }
          }
        """
        expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}}
        result = await graphql(StarWarsSchema, query)
>       assert result == (expected, None)
E       AssertionError: assert ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])]) == ({'node': {'id': 'U2hpcDox', 'name': 'X-Wing'}}, None)
E         +ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])])
E         -({'node': {'id': 'U2hpcDox', 'name': 'X-Wing'}}, None)
E         Full diff:
E         - ({'node': {'id': 'U2hpcDox', 'name': 'X-Wing'}}, None)
E         + ExecutionResult(data={'node': None}, errors=[GraphQLError('Support for returning GraphQLObjectType from resolve_type was removed in GraphQL-core 3.2, please return type name instead.', locations=[SourceLocation(line=3, column=9)], path=['node'])])

tests/test_star_wars_object_identification.py:90: AssertionError

Provide wheel distribution on PyPI

Hi!

Please could this package also by made available as a wheel on PyPI?
https://pypi.python.org/pypi/graphql-relay/0.4.5

Wheels have the following advantages:

  • Faster installation time
  • Allows the wheel cache to work even when using the new pip hash-checking mode
  • Allows tools to statically inspect the package metadata without having to execute untrusted code.
  • ...and more - see: http://pythonwheels.com

This package is pure Python and supports both Python 2 and 3, so can be distributed as a "universal wheel":
https://packaging.python.org/tutorials/distributing-packages/#wheels

To save having to pass --universal to setup.py each time, the pref can be saved in setup.cfg:

[bdist_wheel]
universal=1

The Python packaging guide recommends using Twine to upload packages:
https://packaging.python.org/tutorials/distributing-packages/#uploading-your-project-to-pypi

So with the setup.cfg as above, the steps for building/uploading a new release are then:

$ pip install -U twine setuptools wheel
$ rm -rf dist/ build/
$ python setup.py sdist bdist_wheel
$ twine upload dist/*

The PyPI package listing will then display two entries for the new release, one with type "Source" and the other "Python Wheel".

Many thanks :-)

tag releases

Please add git tags corresponding to the PyPI releases.

Initial Update

The bot created this issue to inform you that pyup.io has been set up on this repo.
Once you have closed it, the bot will open pull requests for updates as soon as they are available.

Breaking change introduced with a patch by changing module names

Problem

There has been introduced a breaking change since the version 3.1.1. Namely, the modules have been renamed to follow the snake_case naming convention in the commit f15bbb.

This causes some runtime (import) errors after installing/updating dependencies:

ModuleNotFoundError: No module named 'graphql_relay.connection.arrayconnection'

in on of the 3rd parties that use this library.

Yes, it uses from graphql_relay.connection.arrayconnection import ... instead of from graphql_relay import ....

I have been able to fix this issue by adding graphql-relay as a project dependency and pinning the version to 3.1.0, but this is not the desired solution, as we do not use the library directly in the project.

Proposed resolution:

One solution would be to allow the old modules reside in the project and mark them as deprecated. Their sole purpose would be to import the contents of the renamed files and allow them to be imported using the old module name.

I can create a Pull request with this patch if this is approved, or with any other proposed solution.

simpler method possible, that distinguishes thanbles from within.

This line has an equivalent method that I think should be one method. if we check if the data is a then-able object
and branch execution from there

def connection_from_list(data, args=None, **kwargs):

def connection_from_promised_list(data_promise, args=None, **kwargs):

Im thinking something like this

if is_thenable(data):
     return Promise.resolve(data).then(
         lambda data: data, len(data)).then(
             lambda data, _len: connection_from_list_slice(
                 data,
                 args,
                 slice_start=0,
                 list_length=_len,
                 list_slice_length=_len,
                 **kwargs))
_len = len(data)
return connection_from_list_slice(
        data,
        args,
        slice_start=0,
        list_length=_len,
        list_slice_length=_len,
        **kwargs
    )

Let me know if there is a specific reason, why there are two methods

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.