Using API calls with VBA and Python to Read and Write to a Database
Using API calls with VBA and Python to write a database is actually a pretty decent architecture. Here, I would like to say that “It takes the best of the both worlds”, but the truth is that it takes the best from Python and somehow through a lot of “magic” it makes the VBA code works as well.
This is how the architecture looks like:

So, we obviously have 3 parts of the architecture:
- Excel with VBA, where the user writes the data and makes GET and POST api calls;
- Python with Flask, responsible to run a server and handle these api calls in a way to write these to a DB;
- A DB, which is supposed to store these calls;
Excel with VBA
The first part of the architecture consists of worksheets and VBA code. The worksheets have the input in a table:

This table is uploaded to the database with a POST api call, through the function MainUpload:
Sub MainUpload()
Dim myLastRow As Long: myLastRow = lastRow(tblRanking.Name)
Dim http As New MSXML2.XMLHTTP60
Dim i As Long
For i = 2 To myLastRow
Dim jsonPerLine As String: jsonPerLine = GenerateJson(i, tblRanking)
http.Open "POST", "http://127.0.0.1:4000/football_ranking", False
http.setRequestHeader "Content-Type", "application/json"
http.send jsonPerLine
If http.Status = 201 Then
Application.StatusBar = "201 OK -> " & tblRanking.Cells(i, 2)
Else
Err.Raise 999, Description:="Line " & i & " Error!"
End If
Next i
Application.StatusBar = ""
End Sub
Public Function GenerateJson(i As Long, tbl As Worksheet) As String
Dim result As String: result = "{"
Dim lastCol As Long: lastCol = LastColumn(tbl.Name)
Dim myCell As Range
For Each myCell In tbl.Range(tbl.Cells(i, 1), tbl.Cells(i, lastCol))
With WorksheetFunction
Dim myKey As String: myKey = .Trim("""" & tbl.Cells(1, myCell.Column) & """")
Dim myVal As String: myVal = .Trim("""" & myCell.Value & """")
End With
If myCell.Column = lastCol Then
result = result & myKey & ":" & Trim(myVal) & "}"
Else
result = result & myKey & ":" & Trim(myVal) & ","
End If
Next
GenerateJson = result
End Function
Well, obviously the function above hopes that there is a running server at http://127.0.0.1:4000 and calling a POST to it would do something. The function GenerateJson() makes JSON, based on the table column names and the i-th value.
Additionally, there is a function called MainDownload() , which looks like this:
Sub MainDownload()
Dim http As New MSXML2.XMLHTTP60
http.Open "GET", "http://127.0.0.1:4000/football_ranking", False
http.send
Dim allTeams As Object
Set allTeams = JsonConverter.ParseJson(http.responseText)
tblDb.Cells.Clear
Dim r As Long: r = 1
Dim c As Long: c = 1
Dim team As Object
For Each team In allTeams
Dim element As Variant
For Each element In team
If r = 1 Then
tblDb.Cells(r, c) = element
Else
tblDb.Cells(r, c) = team(element)
End If
c = c + 1
Next
r = r + 1
c = 1
Next
End Sub
It uses the JsonConvertor class from vba-tools, which actually works pretty decent (thumbs up for the documentation, Tim). What it does is to take the http.responseText and to write it to a worksheet, which has been cleared previously with tblDb.Cells.Clear. So far so good.
Python + Flask, running a server and serving the API
Here the story is a bit easier. Somehow, Python is used to run servers and make API calls. Thus, in general we are having plenty of libraries and two files – main.py and FootballRankingModel.py. I hope that it is easier to understand which one makes what as these two are really small. Anyway, run main.py to start the local server.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import json
from settings import app
db = SQLAlchemy(app)
class FootballRanking(db.Model):
__tablename__ = 'football_ranking'
Id = db.Column(db.Integer, primary_key=True)
Name = db.Column(db.String(100), nullable=False)
Wins = db.Column(db.Integer, nullable=False)
Coach = db.Column(db.String(100))
def json(self):
return {
'Id': self.Id,
'Coach': self.Coach,
'Wins': self.Wins,
'Name': self.Name,
}
def __repr__(self):
football_ranking_object = {
'Id': self.Id,
'Name': self.Name,
'Wins': self.Wins,
'Coach': self.Coach,
}
return json.dumps(football_ranking_object)
def add_line(_name, _wins, _coach):
new_team_line = FootballRanking(
Name=_name,
Wins=_wins,
Coach=_coach
)
db.session.add(new_team_line)
db.session.commit()
def get_all_lines():
return [FootballRanking.json(line) for line in FootballRanking.query.all()]
import json
from flask import Flask, jsonify, request, Response
from FootballRankingModel import *
from settings import *
def football_ranking_object_is_valid(football_ranking):
print(football_ranking)
if ("Wins" in football_ranking) and ("Coach" in football_ranking) and ("Name" in football_ranking):
return True
else:
print("Error from main.football_ranking_object_is_valid")
return False
# GET /football_ranking
@app.route('/football_ranking')
def get_football_ranking():
return jsonify(FootballRanking.get_all_lines())
# POST /football_ranking
@app.route('/football_ranking', methods=['POST'])
def add_line():
request_data = request.get_json()
if football_ranking_object_is_valid(request_data):
print (request_data['Name'])
FootballRanking.add_line(
request_data['Name'],
request_data['Wins'],
request_data['Coach'])
response = Response("OK", 201, mimetype='application/json')
return response
else:
invalid_football_ranking_object = {
"error": "Invalid ns_cos object passed in the request!",
"help_string": "Hello dear friend! Data passed should be a bit similar to the expected JSON!"
}
response = Response(json.dumps(invalid_ns_cos), status=400, mimetype='application/json')
return response
app.run(port=4000, debug=True)
Database
For the database part I have left 2 things – the SQL, used to create the table football_ranking and the settings.py, which shows the path to the Flask app and the DB.
from flask import Flask app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///football_db.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
CREATE TABLE "football_ranking" ( "Id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "Name" TEXT NOT NULL, "Wins" INTEGER NOT NULL, "Coach" TEXT NOT NULL )
At the end conda env export > requirements_py37.yml would export all the libraries and packages, used for the project.
Enjoy everything in Git – https://github.com/Vitosh/Python_personal/tree/master/PythonProjects/VBA_API!