Como consumir uma API GraphQL em Python: passo a passo
Cada vez mais plataformas — GitHub, Shopify, Contentful — expõem os dados por GraphQL em vez de REST. Consumir uma API GraphQL em Python é mais simples do que parece: é sempre um POST para o mesmo URL, com uma query em texto que descreve exatamente os campos que queremos. Segue o caminho completo, do primeiro pedido até um DataFrame pandas pronto a analisar.
Pré-requisitos
- Python 3.9 ou superior instalado.
- As bibliotecas
requestsepandas:pip install requests pandas. - Um endpoint GraphQL. Nos exemplos usamos o público
https://countries.trevorblades.com/graphql, que não pede autenticação. - Noções básicas de JSON e de dicionários em Python.
Passo 1: Perceber o que muda em relação a REST
Numa API REST existem vários URLs (um por recurso) e o servidor decide que campos devolve. Numa API GraphQL há um único URL e um único método, o POST. O que muda de pedido para pedido é o corpo: uma query onde o cliente pede só os campos de que precisa. Isso elimina o clássico problema de receber 40 campos quando só interessam três.
Passo 2: Escrever a primeira query
A query é apenas texto. Começa por escrevê-la e testá-la no explorador do fornecedor (quase todos têm um) antes de a levar para o Python:
query {
countries {
code
name
capital
}
}
Passo 3: Enviar a query com requests
O corpo do pedido é sempre um JSON com a chave query (e, opcionalmente, variables). A resposta útil vem dentro da chave data.
import requests
URL = "https://countries.trevorblades.com/graphql"
QUERY = """
query {
countries {
code
name
capital
}
}
"""
response = requests.post(URL, json={"query": QUERY}, timeout=30)
response.raise_for_status()
data = response.json()
print(data["data"]["countries"][:3])
Repara em dois detalhes que evitam dores de cabeça: json= (e não data=) trata da serialização e do cabeçalho correto, e timeout impede que o script fique pendurado para sempre.
Passo 4: Usar variáveis em vez de concatenar strings
Nunca construas a query com f-strings. Declara variáveis com tipo e envia-as no campo variables — é mais seguro, e o servidor pode reaproveitar o plano da query.
QUERY = """
query CountriesByContinent($code: ID!) {
continent(code: $code) {
name
countries {
code
name
}
}
}
"""
variables = {"code": "EU"}
response = requests.post(
URL,
json={"query": QUERY, "variables": variables},
timeout=30,
)
countries = response.json()["data"]["continent"]["countries"]
print(len(countries))
Passo 5: Autenticar com um token
APIs privadas pedem um token no cabeçalho Authorization. Guarda-o numa variável de ambiente, nunca no código:
import os
import requests
URL = "https://api.github.com/graphql"
headers = {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"}
QUERY = """
query {
viewer {
login
}
}
"""
response = requests.post(URL, json={"query": QUERY}, headers=headers, timeout=30)
print(response.json()["data"]["viewer"]["login"])
Passo 6: Tratar o erro que vem disfarçado de sucesso
Esta é a armadilha número um: uma API GraphQL devolve HTTP 200 mesmo quando a query falha. O raise_for_status() não deteta nada — o erro está na chave errors do JSON. Centraliza tudo numa função:
def run_query(query, variables=None, headers=None):
response = requests.post(
URL,
json={"query": query, "variables": variables or {}},
headers=headers,
timeout=30,
)
response.raise_for_status()
body = response.json()
if "errors" in body:
messages = [e["message"] for e in body["errors"]]
raise RuntimeError("GraphQL: " + "; ".join(messages))
return body["data"]
Se receberesKeyError: 'data', quase de certeza a resposta traziaerrors. Imprime o JSON inteiro antes de te queixares da rede.
Passo 7: Paginar com cursores
GraphQL não usa ?page=1. Pede-se um bloco de N registos e o servidor devolve, dentro de pageInfo, um endCursor e um hasNextPage. O ciclo repete-se até hasNextPage ser falso:
QUERY = """
query Repos($cursor: String) {
viewer {
repositories(first: 50, after: $cursor) {
nodes {
name
stargazerCount
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
"""
all_repos = []
cursor = None
while True:
page = run_query(QUERY, {"cursor": cursor}, headers)["viewer"]["repositories"]
all_repos.extend(page["nodes"])
if not page["pageInfo"]["hasNextPage"]:
break
cursor = page["pageInfo"]["endCursor"]
print(len(all_repos))
Pedir blocos demasiado grandes (por exemplo 1000) costuma dar erro de custo de query. Entre 50 e 100 é o intervalo seguro na maioria das APIs.
Passo 8: Converter o resultado num DataFrame
Como a resposta é JSON aninhado, o json_normalize achata os níveis e deixa a tabela pronta para pandas:
import pandas as pd
df = pd.json_normalize(all_repos)
df.to_csv("repos.csv", index=False)
print(df.head())
Verificar o resultado
Antes de dares o script por fechado, confirma três coisas:
- O
printdo primeiro passo mostra registos reais (e não uma lista vazia). - Uma query propositadamente errada (troca
namepornome) faz orun_querylevantarRuntimeError— sinal de que estás mesmo a ler o campoerrors. - O número de linhas do DataFrame bate certo com o
len(all_repos)e com o total que a plataforma mostra na sua interface.
Conclusão
Com um POST, um dicionário de variáveis, uma verificação de errors e um ciclo de cursores tens tudo o que é preciso para extrair dados de praticamente qualquer API GraphQL. O passo seguinte natural é agendar este script (Azure Functions, Power Automate ou um simples cron) e escrever o resultado num Lakehouse ou numa tabela SQL, em vez de um CSV. E fica a pergunta: das APIs que consomes hoje, quantos campos estás a receber sem nunca usar?