Skip to main content

non intrusive python graphql client library wrapped around pydantic

Project description

ql (in development)

Graphql client library, wrapped around pydantic classes for type validation, provide simple, safe and pythonic way to query data from a graphql api.

using pydantic for creating python objects from rest api is common, it is easy and it has type validation, so why not make it easy also for graphql apis?

features:

  • python objects to valid graphql string
  • http send and recv information
  • scalar query responses

TOC

install

pip3 install pydantic-graphql

what can it do

at the time of writing, the ql library supports only querying data and scalarazing it to the pydantic models, no extra code is required to make your pydantic model compatible with the library.

to get a better image you can take a look at the #query examples

how does it handle http?

http can be different from implementation to implementation, most implementation of graphql are very simple, a single POST request with basic authentication, there is no need for that to be controlled by the library, the whole point of this library is to make it easy to work with pydantic and graphql apis, for how configure http read #http

configure my pydantic model

it is simple, you can just configure your pydantic model like so

import ql
from pydantic import BaseModel

@ql.model
class MyModel(BaseModel):
  ...

querying

querying is the most common operation in any api, we read data more then we mutate it, we will use this simple model for our example

import ql
from pydantic import BaseModel

@ql.model
class Point(BaseModel):
  x: int
  y: int

if we want to query this model from graphql, our request probably will look like this

query {
  Point {
    x,
    y
  }
}

with the ql library it will look like so

import ql 

# define the `Point` model

query_str = ql.query(
  (Point, (
    ql._(Point).x,
    ql._(Point).y
  ))
)

what the heck is the _ function? read here about the function

this python code will convert the python tuple to a valid graphql query that we can use to send graphql, we can print it

print(query_str)
# query{Point{x,y,__typename}}

by default, all query functions will add the __typename field, we can prevent that with passing the query function argument include_typename=False, but we won't do that for now.

the basic structure of a python query tuple is this

(<model>, (
  <field_a>,
  <field_b>,
  ...
))

now how do you deal with nested models?

import ql
from pydantic import BaseModel

@ql.model
class Owner(BaseModel):
  name: str
  age: int

@ql.model
class Shop(BaseModel):
  owner: Owner
  items_count: int

we want to query shop with the owner data, our graphql query will look like so

query {
  Shop {
    items_count,
    owner {
      name
      age
    }
  }
}

our python query will look like so

query_str = ql.query(
  (Shop, (
    ql._(Shop).items_count,
    (ql._(owner), (
      ql._(Owner).name,
      ql._(Owner).age
    ))
  ))
)

you see the pattern? for sub fields we use the same structured tuple, but instead of a model, we give it a field, it this nesting can continue as much as we want.

(<model>, (
  <field_a>,
  <field_b>,
  (<field_c>, (
    <c_model_field_a>,
    <c_model_field_b>
    ...
  ))
))

what if my class name is different from my query name?

by default when you decorate your model with ql.model, the used name in the query is the class name, but lets say we have this case

import ql
from pydantic import BaseModel

@ql.model
class Human(BaseModel):
  name: str

but our query should look like this

query {
  person {    # we need the name `person` instead of human
    name
  }
}

for that ql.model can take the argument query_name which will be used when we query that model

@ql.model(query_name="person")
class Human(BaseModel):
  ...

and we can query regularlly

query_str = ql.query(
  (Human, (
    ql._(Human).name
  ))
)
print(query_str)
# query{person{name, __typename}}

what if my field name is different from my query name?

in case we have this case:

import ql
from pydantic import BaseModel

@ql.model
class Human(BaseModel):
  first_name: str
  middle_name: str
  last_name: str

but our query should look like so

query {
  Human {
    name,  # first_name
    middle,  # middle_name
    last  # last_name
  }
}

we can attach metadata to our field that tells ql, that this field has different query name

...
from typing import Annotated

@ql.model
class Human(BaseModel):
  first_name: Annotated[str, ql.metadata(query_name="first")]
  middle_name: Annotated[str, ql.metadata(query_name="middle")]
  last_name: Annotated[str, ql.metadata(query_name="last")]

we can query regularly and we get our expected results

query_str = ql.query(
  (Human, (
    ql._(Human).first_name,
    ql._(Human).middle_name,
    ql._(Human).last_name
  ))
)
print(query_str)
# query{Human{first,middle,last,__typename}}

query operations

in graphql we have couple of operations that we can use when we query our data

raw query

send a simple query string and get response dict

response = ql.raw_query_response("""
  query {
    Person(name: "bob") {
      name,
      age
    }
  }
""")

scalar response

scalar given graphql response, note that the response must contain the __typename field for any type, thats how the scalar knows which model should be used

arguments

graphql supports arguments when querying, ql supports it too

import ql
from pydantic import BaseModel

@ql.model
class Human(BaseModel):
  name: str
  age: int

query_str = ql.query(
  (ql.arguments(Human, filter="age <= 50"), (
    ql._(Human).name,
    ql._(Human).age
  ))
)
print(query_str)
# query{Human(filter: "age <= 50"){name,age,__typename}}

it is simple as just wrapping our model with ql.arguments

inline fragments

graphql supports inline fragments, this happens when a type can return multiple different types with different fields, ql supports that too

import ql
from pydantic import BaseModel

@ql.model
class Human(BaseModel):
  name: str

@ql.model
class Male(Human):
  working: bool

@ql.model
class Female(Human):
  pregnant: bool

query_str = ql.query(
  (Human, (
    ql._(Human).name,
    (ql.on(Male), (
      ql._(Male).working,
    )),
    (ql.on(Female), (
      ql._(Female).pregnant,
    ))
  ))
)
print(query_str)
# query{Human{name, ...on Male{working,__typename}, ...on Female{pregnant,__typename},__typename}}

http

todo

Query examples

simple query

import ql
from pydantic import BaseModel


@ql.model
class Point(BaseModel):
  x: int
  y: int


q = ql.query(
  (Point, (
    ql._(Point).x,
    ql._(Point).y
  ))
)
print(q)

query{Point{x,y}}

smart implements w nested query w inline fragment

import ql
from pydantic import BaseModel

@ql.model
class Human(BaseModel):
  first_name: str
  last_name: str

@ql.model
class Female(Human):
  pregnant: bool

@ql.model
class Male(Human):
  pass

print(ql.implements(Human))  # what does `Human` implement
q = ql.query(
    (Human, (
        ql._(Human).first_name,
        (ql.on(Female), (
            ql._(Female).pregnant,
        ))
    ))
)
print(q)

frozenset({<class '__main__.Human'>})
query{Human{first_name,...on Female{pregnant,__typename},__typename}}

query with http

import ql
import requests
from pydantic import BaseModel

ql.http.set_request_func(lambda q: requests.get(...).json())

# define models ...

response = ql.query_response(
  (Point, (
     ql._(Point).x,
     ql._(Point).y
  ))
)
print(response)

{"data": {"point": "x": 50, "y": -50}}

query and scalar response

import ql
import requests
from pydantic import BaseModel

ql.http.set_request_func(lambda q: requests.get(...).json())

@ql.model
class Point(BaseModel):
  x: int
  y: int

scalared = ql.query_response_scalar(
 (Point, (
   ql._(Point).x,
   ql._(Point).y
  ))
)
print(scalared)

{"point": Point(x=50, y=-50)}

api

model

query_fields_nt

returns a namedtuple with the model queryable fields, which maps between the model field name, to the defined field query_name.

import ql 
from typing import Annotated
from pydantic import BaseModel

@ql.model
class Human(BaseMode):
  first_name: Annotated[str, ql.metadata(query_name="name")]
  last_name: Annotated[str, ql.metadata(query_name="family_name")]
  age: int

print(ql.query_fields_nt(Human).first_name)  # name
print(ql.query_fields_nt(Human).last_name)   # family_name
print(ql.query_fields_nt(Human).age)  # age

NOTE: because this function is common when querying, there is a function alias _ which just wraps the query_fields_nt, so calling _ is actually calling query_fields_nt

query

query

todo

query_response

todo

query_response_scalar

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_graphql-1.0.0.tar.gz (13.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_graphql-1.0.0-py3-none-any.whl (11.9 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_graphql-1.0.0.tar.gz.

File metadata

  • Download URL: pydantic_graphql-1.0.0.tar.gz
  • Upload date:
  • Size: 13.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.3 CPython/3.12.4 Linux/6.10.4-arch2-1

File hashes

Hashes for pydantic_graphql-1.0.0.tar.gz
Algorithm Hash digest
SHA256 bc9c6fec2b999719fbdd528c77292e9f763061075ed9be7deb5f011c3ef53976
MD5 2cb20fa798ed65a987d3455dd8b1c1df
BLAKE2b-256 3e17b043aec36fc602b65bdb81c8b605c96dee71d2a219b4fd4600d3d4a50881

See more details on using hashes here.

File details

Details for the file pydantic_graphql-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pydantic_graphql-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 11.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.3 CPython/3.12.4 Linux/6.10.4-arch2-1

File hashes

Hashes for pydantic_graphql-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 44249ce94b463ac359609e3e062de0940324d3cf365375b46e518131e70252db
MD5 1d299002802b69bf3ac341417e29ce14
BLAKE2b-256 2c201f71e01a7f7e611d7310c384b57be9435edc9c9d3150c3e5e0bd6ea143bd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page