Skip to content

Python (gql)

Step 1. Getting started

Step 1.1. Overview

After the initial setup in Step 1, you'll have the following APIs at your fingertips.

main.py
from gql import gql, Client
from delivery_tracker_gql.transport import DeliveryTrackerAIOHTTPTransport

# Example query to get the time, code of the last event
# There are many other queries, which can be used to retrieve various information
# Please refer to the documentation and API schema on the official site.
TRACK_QUERY = gql(
"""
query GetTrackLastEvent($carrierId: ID!, $trackingNumber: String!) {
track(carrierId: $carrierId, trackingNumber: $trackingNumber) {
lastEvent {
time
status {
code
}
}
}
}
"""
)

# Create a Client that connects to the DeliveryTracker Server.
client = Client(
transport=DeliveryTrackerAIOHTTPTransport(
client_id="[YOUR_CLIENT_ID]",
client_secret="[YOUR_CLIENT_SECRET]",
),
fetch_schema_from_transport=False,
)

# Call the DeliveryTracker API.
# Use `await client.execute_async(TRACK_QUERY, ...)` inside the coroutine
result = client.execute(
TRACK_QUERY,
variable_values={
"carrierId": "en.cjlogistics",
"trackingNumber": "1234567890",
},
)

print(result)

Step 1.2. Add dependencies

This document walks you through how to use the Delivery Tracker API using python-gql. To do this, first install python-gql.

pip install gql[aiohttp]

Step 1.3. Add Transport class

The AIOHTTPTransport built into python-gql does not provide Delivery Tracker Auth out of the box. For authentication, add a DeliveryTrackerAIOHTTPTransport that inherits from AIOHTTPTransport and implements Delivery Tracker Auth.

delivery_tracker_gql/transport.py
import json
import base64
import aiohttp
from typing import Any, Callable, Dict, Optional, Union
from aiohttp.typedefs import LooseHeaders
from graphql import DocumentNode, ExecutionResult
from gql.transport.aiohttp import AIOHTTPTransport
from gql.transport.exceptions import (
TransportProtocolError,
TransportServerError,
)


class DeliveryTrackerAIOHTTPTransport(AIOHTTPTransport):
DELIVERY_TRACKER_API_GRAPHQL_ENDPOINT = "https://apis.tracker.delivery/graphql"
DELIVERY_TRACKER_AUTH_TOKEN_ENDPOINT = "https://auth.tracker.delivery/oauth2/token"
__credentials: str
__auth_http_session: Optional[aiohttp.ClientSession] = None
__access_token: Optional[str] = None

def __init__(
self,
client_id: str,
client_secret: str,
headers: Optional[LooseHeaders] = None,
timeout: Optional[int] = None,
ssl_close_timeout: Optional[Union[int, float]] = 10,
json_serialize: Callable = json.dumps,
client_session_args: Optional[Dict[str, Any]] = None,
):
self.__credentials = (
"Basic "
+ base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
)
super().__init__(
url=self.DELIVERY_TRACKER_API_GRAPHQL_ENDPOINT,
headers=headers,
timeout=timeout,
ssl_close_timeout=ssl_close_timeout,
json_serialize=json_serialize,
client_session_args=client_session_args,
)

async def execute(
self,
document: DocumentNode,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
extra_args: Optional[Dict[str, Any]] = None,
upload_files: bool = False,
) -> ExecutionResult:
access_token = await self.__get_access_token()

if extra_args is None:
extra_args = {}
if "headers" not in extra_args:
extra_args["headers"] = {}

if access_token is not None:
extra_args["headers"]["Authorization"] = f"Bearer {access_token}"

result = await super().execute(
document=document,
variable_values=variable_values,
operation_name=operation_name,
extra_args=extra_args,
upload_files=upload_files,
)

if self.__has_unauthenticated_error(result):
access_token = await self.__get_access_token(
force_fetch_new_access_token=True
)
if access_token is not None:
extra_args["headers"]["Authorization"] = f"Bearer {access_token}"

result = await super().execute(
document=document,
variable_values=variable_values,
operation_name=operation_name,
extra_args=extra_args,
upload_files=upload_files,
)

return result

def __has_unauthenticated_error(self, execution_result: ExecutionResult) -> bool:
if execution_result.errors is None:
return False
for error in execution_result.errors:
try:
if error["extensions"]["code"] == "UNAUTHENTICATED":
return True
except:
pass
return False

async def __get_access_token(
self, force_fetch_new_access_token: bool = False
) -> Optional[str]:
if self.__access_token is None or force_fetch_new_access_token:
self.__access_token = await self.__fetch_new_access_token()
return self.__access_token

async def __fetch_new_access_token(self) -> Optional[str]:
if self.__auth_http_session is None:
self.__auth_http_session = aiohttp.ClientSession()

auth_response = await self.__auth_http_session.post(
url=self.DELIVERY_TRACKER_AUTH_TOKEN_ENDPOINT,
headers={
"Authorization": self.__credentials,
"Content-Type": "application/x-www-form-urlencoded",
},
data=b"grant_type=client_credentials",
)

if auth_response.status >= 400:
raise TransportServerError(
f"Auth error: http response code={auth_response.status} body={await auth_response.text()}"
)

try:
auth_response_body = await auth_response.json()
access_token = auth_response_body["access_token"]
assert isinstance(access_token, str)
return access_token
except Exception as e:
raise TransportProtocolError("The access_token field was not found.") from e

Step 1.4. Execute your first query

"Step 1.1. Write the main.py code described in the "Overview" and run main.py to get the API working.

Step 2. Advanced

If you need additional information, such as schema validation, please see the python-gql documentation.