Clients

Complete examples of using the QC Analytical Results API from R and from Python: send a dataset, ask for a chart, handle an expired dataset.

The API is plain HTTPS and JSON, so any language will do. Two complete examples. Both read the key from the environment (QC_API_KEY), send a CSV gzipped, ask for a chart, and send the data again if the dataset has expired.

R

Uses httr and jsonlite.

library(httr)
library(jsonlite)

API <- "https://api.geoscience-analytics.com"
auth <- add_headers(Authorization = paste("Bearer", Sys.getenv("QC_API_KEY")))

call <- function(method, path, ...) {
  r <- VERB(method, paste0(API, path), auth, ...)
  body <- fromJSON(content(r, "text", encoding = "UTF-8"), simplifyVector = TRUE)
  if (status_code(r) >= 400) stop(body$error$code, ": ", body$error$message, " (", body$error$request_id, ")")
  body
}

# send the CRM results once (gzipped CSV)
send <- function(file, kind) {
  gz <- tempfile(fileext = ".gz")
  con <- gzfile(gz, "wb"); writeBin(readBin(file, "raw", file.size(file)), con); close(con)
  call("POST", paste0("/v1/datasets?kind=", kind), content_type("text/csv"),
       add_headers(`Content-Encoding` = "gzip"), body = upload_file(gz, type = "text/csv"))
}
ds <- send("crms.csv", "accuracy")
ds$facets$crms   # the CRM ids you can ask for

# a chart; the dataset is sent again if it has expired
chart <- function(type, params) {
  ask <- function() call("POST", paste0("/v1/charts/", type), content_type_json(), body = toJSON(params, auto_unbox = TRUE))
  tryCatch(ask(), error = function(e) {
    if (!grepl("^dataset_expired", conditionMessage(e))) stop(e)
    ds <<- send("crms.csv", "accuracy"); params$dataset_id <<- ds$dataset_id
    ask()
  })
}
sh <- chart("shewhart", list(dataset_id = ds$dataset_id, lab = "ALS", analyte = "Au_AA25_ppm",
                             crm_type = "LAB_STD", crms = I("ALS-26"), nelson_rules = TRUE))
sh$spec$limits            # centre, warning and action limits
head(sh$spec$points)      # one row per result, with its flags
sh$spec$nelson$table      # the eight rules, PASS or FAIL

(I("ALS-26") keeps a single CRM as a JSON array, ["ALS-26"], which crms expects.)

Python

Uses requests.

import gzip
import os
import requests

API = "https://api.geoscience-analytics.com"
HEADERS = {"Authorization": "Bearer " + os.environ["QC_API_KEY"]}


class ApiError(Exception):
    def __init__(self, status, body):
        self.status = status
        self.code = body["error"]["code"]
        super().__init__("%s: %s (%s)" % (self.code, body["error"]["message"], body["error"].get("request_id")))


def call(method, path, **kw):
    headers = dict(HEADERS, **kw.pop("headers", {}))
    r = requests.request(method, API + path, headers=headers, timeout=120, **kw)
    body = r.json()
    if r.status_code >= 400:
        raise ApiError(r.status_code, body)
    return body


def send(path, kind):
    with open(path, "rb") as f:
        data = gzip.compress(f.read())
    return call("POST", "/v1/datasets?kind=" + kind, data=data,
                headers={"Content-Type": "text/csv", "Content-Encoding": "gzip"})


ds = send("duplicates.csv", "precision")
print(ds["facets"]["stages"])


def chart(chart_type, params):
    try:
        return call("POST", "/v1/charts/" + chart_type, json=params)
    except ApiError as e:
        if e.code != "dataset_expired":
            raise
        params["dataset_id"] = send("duplicates.csv", "precision")["dataset_id"]
        return call("POST", "/v1/charts/" + chart_type, json=params)


sc = chart("scatter", {"dataset_id": ds["dataset_id"], "lab": "ALS", "analyte": "Au_AA25_ppm",
                       "stage": "Pulverising Stage", "rd_formula": "MPRD"})
print(sc["rows_used"], "pairs,", sc["spec"]["stats"]["outliers"], "outliers")
for p in sc["spec"]["points"][:3]:
    print(p["ASSAYVALUE_OR"], p["ASSAYVALUE_CK"], p["RD"], p["outlier"])

Patterns worth copying

  • Send on every run, keep the id for the run. The same file gives the same id back, so there is no state to keep between runs.
  • Retry once on dataset_expired. Send the data again and repeat the call.
  • Wait and retry on 429, a 503 or no answer, a few times with a growing pause (5 s, 30 s, 2 min). Nothing else needs a retry: a 4xx other than 429 will fail the same way again.
  • Keep parameters from the answer with anything you store: it records the defaults that were in force.
  • Never log the key. Log the request_id instead; it is what support needs.