Retreiving Data from Reports

This is a complete example of calling the Report API to access the data of a saved report.

This example is written in Python for its simplistic style.

The API is hosted at graph.medproctor.com, our GraphQL server. It has RESTful APIs to complement some of the features the GraphQL queries provide, such as the Reports API of this example.

This example will call to receive data from a named report. A successful call will stream either the CSV or JSON-based results. This is for memory efficiency in case a report returns thousands of records.

The authentication is based on the OAuth Bearer Token. The token is an Access Token with a short time expiration. If you use the access token and get a 401, you can refresh the token with the refresh API.

import urllib.parse
import http.client
import json
import sys
import csv


class AccessToken:
    """The response from the accessToken endpoint"""

    def __init__(self, access_token: str, token_type: str,
                 expires_in: int, refresh_token: str) -> None:
        self.access_token: str = access_token
        self.token_type: str = token_type
        self.expires_in: int = expires_in
        self.refresh_token: str = refresh_token

    def __str__(self) -> str:
        return (f"AccessToken(access_token={self.access_token}, "
                f"token_type={self.token_type}, expires_in={self.expires_in}, "
                f"refresh_token={self.refresh_token})")

The AccessToken class represents the response from the Access Token endpoint. We will use the access_token to authenticate into the Reports API.

# Set the connection object the the Graph API Service
conn = http.client.HTTPSConnection("graph.medproctor.com")

# Preparing the call to the accessToken service

# Create and URL encode the credentials
params = {
    "username": "",
    "password": ""
}

encoded_params = urllib.parse.urlencode(params)

We have prepared the HTTP client to contact https://graph.medproctor.com. The username and password, which will be assigned to you by Med+Proctor, are query parameters and must be URL encoded.

# The Refresh Token endpoint is at `/api/auth/refreshToken``

# Make the call
conn.request(method="GET",
             url=f"/api/auth/accessToken?{encoded_params}")

The call is executed using the /api/auth/accessToken endpoint. This endpoint is delegating the authentication calls to the backend authentication server.

# Get the response object
res = conn.getresponse()

# handle any errors
if res.status != 200:
    print(f"error getting token: {res.status} {res.reason}")
    sys.exit(1)

If there is an error, then a non-200 status will be returned.

# Read the JSON payload
data = res.read()
access_token_json_string = data.decode("utf-8")

# Create the dictionary
access_token_dict = json.loads(access_token_json_string)

This is reading the response body, converting it to a string, then parsing the JSON into a dictionary.

# Create the class object for the access token
access_token = AccessToken(**access_token_dict)

This is constructing the AccessToken class. It assigns each key/value pair of the dictionary as named parameters on the constructor method of the AccessToken class.

Now that we have the token in the AccessToken class, we can assign it as the Bearer token in the following HTTP call.

# Now that we have the access token, we can call the Report API

headers = {
    "Accept": "text/csv",
    "Authorization": f"Bearer {access_token.access_token}"
}

# set the report's name for the data we want to retrieve
name_of_the_report = urllib.parse.quote("compliance", safe='')

Since report names can have spaces and other special characters, we must ensure the URL value is encoded. The report name is case-insensitive and will be used as the path template variable for the endpoint URL.

The endpoint format is: /api/report/{report name | report id}?pageNumber=x&pageSize=y

The mandatory report name or report id is the report's name or GUID ID.

pageNumber and pageSize query parameters are optional. They are used to provide paging.

# request the data
conn.request(
    method="GET", url=f"/api/report/{name_of_the_report}?pageNumber=1", headers=headers)

# get the response object
res = conn.getresponse()

# handle the errors
if res.status != 200:
    print(f"error fetching data: {res.status} {res.reason}")
    sys.exit(2)

The call is made, and if there's an error, it's reported, and the program ends.

At this point, the CSV data will start to stream. This example has provided a class that represents the columns of the report. For simplicity, all of the columns are typed as a string. In a real scenario, you may convert to numerics and dates as needed.

# A class to represent the report records.  You can use a dictionary.
# This is just an example of using types

class ReportRecord:
    """The record from the report"""

    # We are breaking idiomatic Python variable naming rules
    # here to be able to match the column names.
    # This helps to have a 'one-line' to create this class.
    def __init__(self, UserName: str, StudentId: str,
                 DOB: str, MCV41: str, MCV42: str, TagMCV4Waived: str,
                 Tdap1: str, Tdap2: str, Tdap3: str,
                 VerifiedDate: str, Covid1: str, Covid2: str
                 ) -> None:
        self.UserName: str = UserName
        self.StudentId: str = StudentId
        self.DOB: str = DOB
        self.MVC41: str = MCV41
        self.MVC42: str = MCV42
        self.TagMCVWaived: str = TagMCV4Waived
        self.Tdap1: str = Tdap1
        self.Tdap2: str = Tdap2
        self.Tdap3: str = Tdap3
        self.VerifiedDate: str = VerifiedDate
        self.Covid1: str = Covid1
        self.Covid2: str = Covid2

    def __str__(self) -> str:
        return (f"ReportRecord(UserName={self.UserName}, "
                f"StudentId={self.StudentId}, DOB={self.DOB}, "
                f"MVC41={self.MVC41}, MVC42={self.MVC42}, "
                f"TagMVC4Waived={self.TagMCVWaived}, Tdap1={self.Tdap1}, "
                f"Tdap2={self.Tdap2}, Tdap3={self.Tdap3}, "
                f"VerifiedDate={self.VerifiedDate}, Covid1={self.Covid1}, "
                f"Covid2={self.Covid2})"
                )

The following section is how to process an HTTP Chucking stream using the built-in HTTP client. Other third-party HTTP clients, such as Requests, make this process easier.

# The Reports Rest API streams the results using Chunks.
# This makes for efficient memory usage, being we are only reading from the
# HTTP stream just enough to find a record to be processed.


def process_stream(response):
    # Stream the response, reading in chunks
    chunk_size = 1024  # Define your chunk size (in bytes)
    buffer = ''
    reader = None

    while True:
        chunk = response.read(chunk_size).decode('utf-8')
        if not chunk:
            break

        buffer += chunk
        while True:
            line_end = buffer.find('\n')
            if line_end == -1:
                break
            line = buffer[:line_end]
            buffer = buffer[line_end + 1:]

            # Skip header or empty lines
            if reader is None:
                reader = csv.reader([line])
                next(reader)
                continue
            elif not line.strip():
                continue

            # Process each CSV line here

            # copy the CSV record into the class
            record = ReportRecord(*next(csv.reader([line])))

            # Print the class.
            print(record)


# Process the HTTP response
process_stream(res)