Introduction
Open data APIs provide us with the ability to query large data sets in meaningful ways, drill down to the data being requested in our library and present it to the caller with all relevant context. To get information about specific vehicles, we need to use a car data API. These APIs provide detailed information about vehicle brands and models, as well as dealership data like a sale price.
The CIS Automotive API provides us with all sorts of valuable data about the current sales data of tens of thousands of dealerships. We can write a small library that will make use of this API to quickly get the vehicle information we’re looking for.
How to use the Car Data API
Today we’re going to be using the API to provide a small command-line interface that, when given a car brand and model, we’re able to see how much the car is listed for, how much it’s actually sold for, and how many days it takes, on average, to sell. While there are many different vehicle APIs to choose from, for our specific use case the CIS Automotive API is perfect.
When we’re done, we’d like to have an easy to use command-line tool that gives us fast responses in an easy to read format.
Prerequisites
To follow along with this tutorial, you’ll need a text editor, terminal, and a ruby executable. These are all readily available if you’re using Mac OSX. If you are using Windows, you can get set up following the installer directions on sites like RubyInstaller.
Lastly, we’ll need authenticated access to the CIS Automotive car data API. You can attain this by signing up on RapidAPI. I used my Github account to authenticate and after following a few steps in the signup flow, I was ready to go. I’ll be referring to the documentation of the API, throughout the rest of the guide.
Step 1 – Connecting to the API
Starting out, we want to make sure our authentication data and connectivity are prepared for us to start requesting the car data we’re looking for.
We can get started with a small amount of boilerplate code that tests our authentication. There are a few key pieces of data needed for this script:
- Your RapidAPI key
- A subscription to the CIS Automotive API
#$> ruby ~/code/cis_app.rb #!/usr/bin/env ruby require 'uri' require 'net/http' require 'openssl' url = URI("https://cis-automotive.p.rapidapi.com/salePrice?brandName=Ford®ionName=REGION_STATE_CA") http = Net::HTTP.new(url.host, url.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE request = Net::HTTP::Get.new(url) request["x-rapidapi-host"] = 'cis-automotive.p.rapidapi.com' request["x-rapidapi-key"] = '3c0_your_api_key_b9f' response = http.request(request) puts response.read_body
The assignment of the `x-rapidapi-key` header on line 14 of the script needs to use your actual key that appears in the “Header Parameters” section of the CIS Automotive API documentation page.
After confirming our script is correct, we can run it to test the connectivity.
(this process can be copied directly from the “Code Snippet” pane of the documentation page once you’ve selected `Ruby > net::http` from the dropdown.)
$> ruby ~/code/cis_app.rb {"message":"You are not subscribed to this API."}
To get live data, we need to subscribe to the API and start requesting the specific endpoints we’re interested in.
Step 2 – Subscribing to the API and getting car data
To subscribe to the CIS Automotive car database API on RapidAPI, we need to select a Pricing plan. By clicking on the “Pricing” tab, we can select a RapidAPI plan that will fit the capacity we expect we’ll need for our script.
After subscribing to the API, we can run our script again.
$> ruby ~/code/cis_app.rb {"brandName":"Ford","modelName":null,"regionName":"REGION_STATE_CA","condition":"new","msg":null,"cacheTimeLimit":3600,"data":[...]}
Excellent! We’ve got an authenticated connection to the API and we’re pulling down real car data about the parameters we provided. Next we’ll structure this data into ruby objects we can work with more easily.
Step 3 – Structure the API car data
By defining a class for the car data we’re requesting, we’ll be able to instantiate these objects in place of the array of JSON we’re receiving from the API. We can start by taking one entry in the `“data”` array from our response, and structure our sale price object around that.
{"data":[{"name":"Ecosport","average":"23925.3","median":"23940.0","stdDev":"2557.32","pVariance":"6452704.61"}, … ]}
It looks like we’ll want to capture the name, average, median, standard deviation, and variance. This lines up with the description of the endpoint in the RapidAPI description: “Average, median, standard deviation, and population variance of the sale price of new vehicles over the last 15 days for a given brand and region.”
We can easily map the JSON object to ruby by defining the initialization parameters to match the keys provided by the API. This allows us to easily instantiate our new class simply by symbolizing the keys received in the request-response.
class SalePrice attr_accessor :name, :average, :median, :std_dev, :p_variance def initialize(name:, average:, median:,stdDev:,pVariance:) @name = name @average = average @median = median @std_dev = stdDev @p_variance = pVariance end end
Then to make use of the ruby object, we’ll need to refactor the script to a function call that will return an array of SalePrice objects.
require 'json' def get_sale_price(request_url, params) url = URI(request_url) url.query = URI.encode_www_form(params) http = Net::HTTP.new(url.host, url.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE request = Net::HTTP::Get.new(url) request["x-rapidapi-host"] = 'cis-automotive.p.rapidapi.com' request["x-rapidapi-key"] = '3c0_my_api_key_b9f' response = http.request(request) content = JSON.parse(response.read_body, symbolize_names: true) content[:data].map! {|entry| SalePrice.new(entry) } end # Invoked like this: get_sale_price("https://cis-automotive.p.rapidapi.com/salePrice", { brandName: 'Ford', regionName: 'REGION_STATE_CA' })
At this stage, we’ve built a convenient function to request an API endpoint, and receive an array of easy to use ruby objects back. Next we’ll refactor our current script to accommodate more endpoints and ruby objects in a consistent pattern; an API client.
Step 4 – Building the API Client
By extending the refactoring we did with the sale price endpoint, we’ll be able to get a ruby object that provides easy to use functions we can call to return the data we’re looking for. We’ll call this our API Client.
require 'uri' require 'net/http' require 'openssl' require 'json' class APIClient HOST = 'cis-automotive.p.rapidapi.com'.freeze API_KEY = '3c0_my_api_key_b9f'.freeze def sale_price(brand) content = get("/salePrice", brandName: brand) content[:data].map! {|entry| SalePrice.new(entry) } end private def get(path, params) response = request(path, {regionName: 'REGION_STATE_CA'}.merge(params)) content = JSON.parse(response.read_body, symbolize_names: true) end def request(path, params) url = URI("https://#{HOST}#{path}") url.query = URI.encode_www_form(params) http = Net::HTTP.new(url.host, url.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE request = Net::HTTP::Get.new(url) request["x-rapidapi-host"] = HOST request["x-rapidapi-key"] = API_KEY response = http.request(request) end end
By moving the `request` functionality into a generic function, we can remove the need for the application code to worry about the URL or request headers, it can now get the data we’re looking for simply by providing the variable data we care about, the brand of the vehicle we’re looking up in the vehicle data API.
client = APIClient.new price = client.sale_price('Ford') #=> [#<SalePrice:0x007fccc1141078 @name="Ecosport", @average="23369.03", @median="23326.0", @std_dev="2702.7", @p_variance="7186787.48">, … ]
This is a great start, let’s add some more methods to our client for the other endpoints we care about. We wanted to be able to list the sale price difference from the list price, we can get the list price at `/listPrice`. By looking at the RapidAPI documentation, we can see it also accepts the `brandName` parameter and returns data in the same format as `/salePrice`. The only changes we need to make is to add a function to the API client and make a `ListPrice` class.
def list_price(brand) content = get("/listPrice", brandName: brand) content[:data].map! {|entry| ListPrice.new(entry) } end client = APIClient.new price = client.list_price('Ford') #=> [#<ListPrice:0x007f831a034ae0 @name="Ecosport", @average="24789.32", @median="24545.0", @std_dev="2608.12", @p_variance="6783718.49">, ... ]
Lastly, we need to find out how many days it takes on average to sell the vehicle. RapidAPI provides that car data at the `daysToSell` endpoint. Just like the list price endpoint, we’ll need to add the function to our API client, but this time, since the data is structured differently, we’ll also need to create a new class to represent the data we get back from the API.
def days_to_sell(brand) content = get("/daysToSell", brandName: brand) content[:data].map! {|entry| DaysToSell.new(entry) } end #=> [#<DaysToSell:0x007fc02587bb48 @name="C-Max", @average="11.5", @median="11.5", @std_dev="12.02", @p_variance="72.25", @region_average="11.5">, … ]
Step 5 – Building the executable
Now that we’ve got our API client available, let’s make an executable for our command-line interface. We need to require the classes we’ve written so far, build an `App` class that will manage our API calls, and introduce a small amount of logic to handle the parameters provided by the CLI.
# File: ~/code/app.rb class App def find(brand, model) sale_prices = client.sale_prices(brand) list_prices = client.list_prices(brand) days_to_sell = client.days_to_sell(brand) return { sale_price: find_by_model(sale_prices, model), list_price: find_by_model(list_prices, model), sell_data: find_by_model(days_to_sell, model) } end def present(brand, model, data) puts "Searching for all #{brand} #{model}..." puts "List: $#{data[:list_price].average}" puts "Sale: $#{data[:sale_price].average}" puts "Difference: $#{'%.2f' % (data[:list_price].average.to_f - data[:sale_price].average.to_f)}" puts "Days to Sell: #{data[:sell_data].average}n" end private def find_by_model(data, model) data.find{|entry| entry.name.downcase == model.downcase} end def client @client ||= APIClient.new end end # File: ~/code/cis #!/usr/bin/env ruby Dir['./lib/**'].each{|f| load f } require_relative 'app' brand, model = *ARGV if brand.nil? || model.nil? puts "Usage: cis brand model" exit(1) end app = App.new data = app.find(brand, model) app.present(brand, model, data)
To make our `cis` file executable by name, we need to adjust the executable flag
$> chmod +x ~/code/cis
Let’s try it out!
$> cis Ford Ecosport Searching for all Ford Ecosport... List: $24810.39 Sale: $23715.61 Difference: $1094.78 Days to Sell: 168.36
You are now set up with the CIS Automotive car data API! This code can also be found in the github repo for this article.
treat your car like child