Solving a simple use case with Azure Functions – Part 1

The first part of an Azure Functions series, applied on a simple use case: Connect to a public API, parse and display data. Here, a local prototype is created, in a test driven development approach.

Ok, so let’s have some fun with some code in the cloud tonight. I will take a simple use case and I will implement a solution with Azure Functions, just to show how easy and versatile this is.

The use case will be to create a custom web page that will present the latest price for a stock. The needed information will be extracted from a public Finance Information API.

However, what I will do, can easily be generalized and applied to any public or private API, the extracted information can be stored in a database instead of being displayed in a web page, etc.

The workflow

First, let’s lay out the desired workflow:

  1. When I want to know the latest stock price, I will open a specific URL with a browser
  2. At the backend, the public Finance Information API will be called and the information will be extracted
  3. The formatted information will be presented at the web page

This is the simplest workflow to start with. I might improve it later with caching, preemptive lookup, etc.

The output

Let’s start with what I would like to see in my custom page.

To keep things simple, I will display the latest price of the chosen company’s stock, the change percentage from the previous close and the date/time that corresponds to this price. Something like that:

MSFT: $255.02 (+0.13%) 02/12/2022 21:00:00 GMT

I chose the symbol MSFT, that corresponds to Microsoft’s stock, for this lab.

Analyze the source API

The Finance Information API that I will use is Yahoo! Finance, because it’s simple and free.

It is possible to ask for multiple stock prices at the same time, but for this lab, I will use a simple quote query, like so:

https://query1.finance.yahoo.com/v7/finance/quote?symbols=MSFT

The result comes in JSON format and contains lots of information for the stock (formatted for clarity here):

{
  "quoteResponse": {
    "result": [
      {
        "language": "en-US",
        "region": "US",
        "quoteType": "EQUITY",
        "typeDisp": "Equity",
        "quoteSourceName": "Nasdaq Real Time Price",
        "triggerable": true,
        "customPriceAlertConfidence": "HIGH",
        "currency": "USD",
        "marketState": "CLOSED",
        "firstTradeDateMilliseconds": 511108200000,
        "priceHint": 2,
        "postMarketChangePercent": -0.0862682,
        "postMarketTime": 1670029189,
        "postMarketPrice": 254.8,
        "postMarketChange": -0.220001,
        "regularMarketChange": 0.33000183,
        "regularMarketTime": 1670014804,
        "regularMarketDayHigh": 256.05,
        "regularMarketDayRange": "249.75 - 256.05",
        "regularMarketDayLow": 249.75,
        "regularMarketVolume": 21528520,
        "financialCurrency": "USD",
        "exchange": "NMS",
        "shortName": "Microsoft Corporation",
        "longName": "Microsoft Corporation",
        "messageBoardId": "finmb_21835",
        "exchangeTimezoneName": "America/New_York",
        "exchangeTimezoneShortName": "EST",
        "gmtOffSetMilliseconds": -18000000,
        "market": "us_market",
        "esgPopulated": false,
        "regularMarketChangePercent": 0.12956999,
        "regularMarketPrice": 255.02,
        "epsCurrentYear": 9.55,
        "twoHundredDayAverageChangePercent": -0.03831444,
        "priceEpsCurrentYear": 26.703665,
        "sharesOutstanding": 7454470144,
        "bookValue": 23.276,
        "fiftyDayAverage": 237.8712,
        "fiftyDayAverageChange": 17.148804,
        "fiftyDayAverageChangePercent": 0.07209281,
        "twoHundredDayAverage": 265.18024,
        "twoHundredDayAverageChange": -10.160233,
        "marketCap": 1901039058944,
        "forwardPE": 22.810375,
        "priceToBook": 10.95635,
        "sourceInterval": 15,
        "exchangeDataDelayedBy": 0,
        "averageAnalystRating": "1.8 - Buy",
        "tradeable": false,
        "cryptoTradeable": false,
        "regularMarketPreviousClose": 254.69,
        "bid": 254.8,
        "ask": 254.94,
        "bidSize": 10,
        "askSize": 14,
        "fullExchangeName": "NasdaqGS",
        "regularMarketOpen": 249.82,
        "averageDailyVolume3Month": 29578579,
        "averageDailyVolume10Day": 24021410,
        "earningsTimestamp": 1666733400,
        "earningsTimestampStart": 1674471540,
        "earningsTimestampEnd": 1674820800,
        "trailingAnnualDividendRate": 2.54,
        "trailingPE": 27.451023,
        "trailingAnnualDividendYield": 0.009972908,
        "epsTrailingTwelveMonths": 9.29,
        "epsForward": 11.18,
        "fiftyTwoWeekLowChange": 41.59001,
        "fiftyTwoWeekLowChangePercent": 0.19486488,
        "fiftyTwoWeekRange": "213.43 - 344.3",
        "fiftyTwoWeekHighChange": -89.27998,
        "fiftyTwoWeekHighChangePercent": -0.2593087,
        "fiftyTwoWeekLow": 213.43,
        "fiftyTwoWeekHigh": 344.3,
        "dividendDate": 1678320000,
        "displayName": "Microsoft",
        "symbol": "MSFT"
      }
    ],
    "error": null
  }
}

After looking at the data and some investigation, I see that in order to display the information I need, I have to extract the values of the following fields:

InformationField
latest priceregularMarketPrice
change from previous closeregularMarketChangePercent
date/time of latest priceregularMarketTime

The date/time is given in Unix timestamp format, so I will have to convert it to human readable form.

How to read and parse the data

Reading and parsing the json data seems like an easy task. It will be easier doing it in a programming language that I am familiar with. At the time of this writing, the following languages (and versions) can be used with Azure Functions:

LanguageRuntime 4.x
C#.NET 6.0/7.0 and .NET Framework 4.8
F#.NET 6.0/7.0
JavaJava 11 & 8
Python3.7, 3.8, 3.9
PowerShell7.0, 7.2
JavaScriptNode.js 14/16
TypeScript(supported through transpiling to JavaScript)

Notes:

  • Only Generally Available (fully supported and approved for production use) options are listed
  • Only the latest runtime is listed

I will use Python for this lab, but I might revisit it again in the future and try the implementation with the other options (maybe compare them as well).

Create a project to work with

First, I will create a simple project, to write and test some code and make sure that I have the basic functionality in place, before moving on to Azure Function specifics.

A Linux environment will be used for this lab, but the workflow is similar in a windows environment as well.

I will start by setting up a GitHub repository tlnl-001-azfunc-py based on a Python template and then, I will clone the GitHub repository to my workstation to start working:

git clone git@github.com:gpap/tlnl-001-azfunc-py.git

Then I will setup the python virtual environment and activate it:

cd tlnl-001-azfunc-py
python3 -m venv .venv --prompt "tlnl-001-azfunc-py"
source .venv/bin/activate

After that, I can fire up Visual Studio Code by:

code .

Make a prototype

Let’s put the JSON output from the finance API into a file named test_api_sample_msft.json, to start prototyping my code:

curl https://query1.finance.yahoo.com/v7/finance/quote?symbols=MSFT > api_output_sample_msft.json

Or, you can just copy and paste the output you got before into the file manually, if you’re that kind of person 🙂

Let’s write some code to extract the information that I want from this JSON file. What I need is to:

  • read the contents of the file
  • parse the data and find the information I need
  • display the information in the desired format

It’s always a good practice to write tests for your code. In fact, it’s even better to write your tests before writing the code to be tested (that’s a development style known as Test Driven Development). So, I’ll start with some simple tests in a file test.py:

import unittest
import stock  # our code will be here

class TestStock(unittest.TestCase):

  # test parameters and expected data
  params = {
    'data_filename' : 'test_api_sample_msft.json',
    'data' : {
      'symbol' : 'MSFT',
      'regularMarketPrice' : 255.02,
      'regularMarketChangePercent' : 0.12956999,
      'regularMarketTime' : 1670014804
    }
  }

  # test if input contains the expected fields and we were able to read them
  def test_read_file(self):
    data_filename = self.params['data_filename']
    data_expected = self.params['data']
    data = stock.get_stock_data_from_file(data_filename, data_expected.keys())
    for field, value in data_expected.items():
      self.assertIn(field, data)

  # test if the returned data values are the expected ones
  def test_data(self):
    data_filename = self.params['data_filename']
    data_expected = self.params['data']
    data = stock.get_stock_data_from_file(data_filename, data_expected.keys())
    for field, value in data_expected.items():
      self.assertEqual(data[field], value)


if __name__ == "__main__":
  unittest.main()

And let’s write the simplest code to pass my tests in a file stock.py:

import json

# read from a filename containing Yahoo Finance stock data,
# find specified keys and return their values in a dictionary
def get_stock_data_from_file(filename, keys = None):
  if not keys:
    keys = [ 'symbol', 'regularMarketPrice', 'regularMarketChangePercent', 'regularMarketTime' ]
  try:
    with open(filename, 'r') as f:
      data_in = json.load(f)
      data_out = {}
      for key in keys:
        data_out[key] = data_in['quoteResponse']['result'][0][key]
      return data_out
  except Exception as e:
    print("error getting data from file: {}".format(repr(e)))
    return None

Let’s run the test:

python test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
​
OK

More prototyping – display data in simple text and HTML

Now, I will add some code that will display the data on the console and also format the data as simple HTML.

Writing tests for the output might seem like an overkill at this point, but it’s a good habit to get into. You don’t want to end up with code that does all the business logic right but fails to display the desired data.

Having said that, care should be taken to test just for the presence of important information and not for superficial details.

First add the tests in test.py:

import time

...

  # test if text display function contains the data we want
  def test_output_text(self):
    data_filename = self.params['data_filename']
    data_expected = self.params['data']
    data = stock.get_stock_data_from_file(data_filename, data_expected.keys())
    out = stock.view_as_text(data)
    self.assertIn(data['symbol'], out)
    self.assertIn("${:.2f}".format(data['regularMarketPrice']), out)
    self.assertIn("{:2.2f}%".format(data['regularMarketChangePercent']), out)
    self.assertIn(time.strftime("%d/%m/%Y %H:%M:%S %Z", time.localtime(data['regularMarketTime'])), out)

  # test if html display function contains the data we want
  def test_output_html(self):
    data_filename = self.params['data_filename']
    data_expected = self.params['data']
    data = stock.get_stock_data_from_file(data_filename, data_expected.keys())
    out = stock.view_as_html(data)
    self.assertIn(data['symbol'], out)
    self.assertIn("${:.2f}".format(data['regularMarketPrice']), out)
    self.assertIn("{:2.2f}%".format(data['regularMarketChangePercent']), out)
    self.assertIn(time.strftime("%d/%m/%Y %H:%M:%S %Z", time.localtime(data['regularMarketTime'])), out)

The above tests do not account for things like variable precision needed for very small amounts, or handling different currencies and locales, but, this is just a prototype for a specific use case. I can add more use cases later and generalize the code to handle all these stuff, and more.

Then, I add the actual output functions in python.py, to pass the tests:

import time

...

def view_as_text(data):
  template = "{}: ${:.2f} ({}{:2.2f}%) on {}"
  return template.format(data['symbol'],
                         data['regularMarketPrice'],
                         '+' if data['regularMarketChangePercent'] >= 0 else '',
                         data['regularMarketChangePercent'],
                         time.strftime("%d/%m/%Y %H:%M:%S %Z", time.localtime(data['regularMarketTime'])))

def view_as_html(data):
  template = """<!DOCTYPE html>
<html>
  <head>
    <style type="text/css">
      body { font-family:Calibri, Arial, Sans; }
      .price { font-size: normal; }
      .symbol { font-weight:bold; color:blue; }
      .price { font-weight:bold; }
      .timeref { font-size: small; }
    </style>
  </head>
  <body>
    <div id="stock">
      <p class="price"><span class="symbol">{{symbol}}</span>: $<span class="price">{{regularMarketPrice}}</span> <span class="pc">({{regularMarketChangePercent}})</span></p>
      <p class="timeref"><span class="time">{{regularMarketTime}}</span></p>
    </div>
  </body>
</html>"""

  out_html = template
  out_html = out_html.replace('{{symbol}}', data['symbol'])
  out_html = out_html.replace('{{regularMarketPrice}}', "${:.2f}".format(data['regularMarketPrice']))
  out_html = out_html.replace('{{regularMarketChangePercent}}', "{}{:2.2f}%".format('+' if data['regularMarketChangePercent'] >= 0 else '',
                                                                                    data['regularMarketChangePercent']))
  out_html = out_html.replace('{{regularMarketTime}}', time.strftime("%d/%m/%Y %H:%M:%S", time.localtime(data['regularMarketTime'])))
  return out_html

I can already see some optimizations that could be done, but I will leave the refactoring for later.

Run the tests and see them pass:

python test.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

More prototyping – read from API

Now that I have some working prototype code, let’s add some code to read live data from the API!

Add this test in test.py:

 # test if input from api read contains the expected fields and we were able to read them
  def test_read_from_api(self):
    data_expected = self.params['data']
    data = stock.get_stock_data_from_api(data_expected['symbol'], data_expected.keys())
    for field, value in data_expected.items():
      self.assertIn(field, data)

Run the test, fail, then implement the missing function in stock.py:

import requests

...

# read stock data from Yahoo Finance API,
# find specified keys and return their values in a dictionary
def get_stock_data_from_api(symbol, keys = None):
  if not keys:
    keys = [ 'symbol', 'regularMarketPrice', 'regularMarketChangePercent', 'regularMarketTime' ]
  # prepare url and some headers to keep the api happy
  url = "https://query1.finance.yahoo.com/v7/finance/quote?symbols={}".format(symbol)
  headers = { 'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36' }

  # handle the request to the API
  try:
    response = requests.get(url, headers=headers)
    if response.status_code != 200:
      print("error getting stock price: {} - {}".format(response.status_code, response.reason))
      return None
  except requests.exceptions.RequestException as e:
    print("error getting stock price: {}".format(e))
    return None

  # handle the data returned from the API
  try:
    data_in = json.loads(response.text)
    if data_in['quoteResponse']['error']:
      print("error from api: {}", data_in['quoteResponse']['error'])
      return None
    data_out = {}
    for key in keys:
      data_out[key] = data_in['quoteResponse']['result'][0][key]
    return data_out
  except IndexError as e:
    print("error getting data from API for {}: no data".format(symbol))
    return None
  except KeyError as e:
    print("error getting data from API for {}: {}".format(symbol, repr(e)))
    return None

There’s some quick and dirty error handling code in there, just to cover my bases and know what happened in case something goes wrong.

Also, there’s some repeated code from the file reading function, and I should probably split the function in two (one for reading from the API and one for extracting the data) but I’ll deal with code optimization and cleaning later.

If I run the test, it will fail, because I don’t have the requests module installed, as I started with a new virtual environment:

ModuleNotFoundError: No module named 'requests'

Let’s install it (dependencies will be installed automatically) and update the environment’s requirements:

pip install requests
pip freeze > requirements.txt

Now, let’s run the test again:

python test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.689s

OK

Wrap up the prototype

So, with all the tests and prototype code done, I can now make a simple console app in get_stock_price.py:

import sys
import stock

# read command line argument as stock to query for
symbol = sys.argv[1] if len(sys.argv) > 1 else 'MSFT'

if symbol == 'test':
  data = stock.get_stock_data_from_file('test_api_sample_msft.json')
else:
  data = stock.get_stock_data_from_api(symbol)

if data:
  print(stock.view_as_text(data))

Now, I can run this and enjoy the results of my hard work:

python get_stock_price.py test
MSFT: $255.02 (+0.13%) on 02/12/2022 23:00:04 EET
python get_stock_price.py MSFT
MSFT: $245.42 (-0.80%) on 09/12/2022 23:00:04 EET
python get_stock_price.py AAPL
AAPL: $142.16 (-0.34%) on 09/12/2022 23:00:04 EET
python get_stock_price.py TLNL
error getting data from API for TLNL: no data

I will now commit the code as a working local prototype and push to GitHub repository:

git add .
git commit -m 'first working local prototype'
git push

Let’s cloud it

Now that I have the local prototype, I can move on with setting up the Azure Functions environment and code.

This will be the subject of the next part of this series: Lab 001 – Solving a simple use case with Azure Functions – Part 2.

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *