Friday, 20 July 2018

Python - HTML - pytidylib does not install HTML Tidy

So last post I wrote Python class to decompile a *.chm compiled help file. Found within is what looks like HTML 3.2 that be should upgraded to either XHTML or HTML5. I had written some VBA code to do this but since Python month on this blog I am keen to find out what a Python developer would do. They would (I should imagine) use the library https://pypi.org/project/pytidylib/ which wraps the venerable HTML Tidy.

pip install pytidylib does not install HTML Tidy

So one installs pytidylib from a command window with admin rights using

pip install pytidylib
C:\Users\Simon\source\repos\foo\bar>pip install pytidylib
Collecting pytidylib
  Downloading https://files.pythonhosted.org/packages/2d/5e/4d2b5e2d443d56f444e2a3618eb6d044c97d14bf47cab0028872c0a468e0/pytidylib-0.3.2.tar.gz (87kB)
    100% |████████████████████████████████| 92kB 1.4MB/s
Installing collected packages: pytidylib
  Running setup.py install for pytidylib ... done
Successfully installed pytidylib-0.3.2

And Using Visual Studio I run a small example program to test the install

from tidylib import tidy_document
document, errors = tidy_document('''<p>fõo <img src="bar.jpg">''',
options={'numeric-entities':1})
print (document)
print (errors)

But unfortunately it complains of not being able to find libtidy which indicates HTML Tidy is not installed for you.

Here is the stack trace

OSError
  Message=Could not load libtidy using any of these names: libtidy,libtidy.so,libtidy-0.99.so.0,cygtidy-0-99-0,tidylib,libtidy.dylib,tidy
  StackTrace:
C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python36_64\lib\site-packages\tidylib\tidy.py:99 in Tidy.__init__
C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python36_64\lib\site-packages\tidylib\tidy.py:234 in get_module_tidy
C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python36_64\lib\site-packages\tidylib\tidy.py:222 in tidy_document
C:\Users\Simon\source\repos\CompiledHelpToEbookPythonApp\CompiledHelpToEbookPythonApp\HtmlTidy.py:3 in 

Install HTML Tidy Binaries

It is required to install the HTML Tidy Binaries separately. I got mine from http://binaries.html-tidy.org/. Initially, I took the 32-bit edition which was a mistake and the error persisted. So I took the 64-bit edition, I downloaded tidy-5.6.0-vc14-64b.zip, extracted it and then added the extracted bin folder to my path. Don't forget to restart processes for the environment variables changes to be picked up.

After Successful Install

After successful install this is what is output from the sample program above.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
  <head>
    <title></title>
  </head>
  <body>
    <p>fõo <img src="bar.jpg">
  </body>
</html>

line 1 column 1 - Warning: missing <!DOCTYPE> declaration
line 1 column 1 - Warning: plain text isn't allowed in <head> elements
line 1 column 1 - Info: <head> previously mentioned
line 1 column 1 - Warning: inserting implicit <body>
line 1 column 1 - Warning: inserting missing 'title' element

Press any key to continue . . .

Python - VBA - HTML Help - COM Callable Python class runs *.chm decompiler.

I have many old *.chm compiled help files on my computer and for some reason the help viewer is bust. I am considering writing (Python) code which will upgrade these *.chm files into ebooks. The first module is given here, it will run the Microsoft HTML Help executable (hh.exe) to decompile a *.chm file into its constituent html files all within working subdirectories in the %temp% folder.

Submitting to Code Review

I might very well complete a full application and place in github. This means I need to raise my Python standards and so I have submitted this module to codereview.stackexchange.com

The HelpFileDecompiler Python class

Here is the Python code

class HelpFileDecompiler(object):
    _reg_clsid_ = "{4B388A08-8CAB-4568-AF78-47032744A368}"
    _reg_progid_ = 'PythonInVBA.HelpFileDecompiler'
    _public_methods_ = ['DecompileHelpFileMain']

    def DecompileHelpFileMain(self, sHelpFile):
        import os.path
        eg = ", e.g. 'c:\\foo.chm'"
        if not isinstance(sHelpFile, str):
            raise Exception("sHelpFile needs to be a string" + eg)
        if len(sHelpFile) == 0:
            raise Exception("sHelpFile needs to be a non-null string" + eg)
        if sHelpFile.lower()[-4:] != ".chm":
            raise Exception("sHelpFile needs to end with '.chm' " + eg)
        if not os.path.isfile(sHelpFile):
            raise Exception("sHelpFile " + sHelpFile + " not found")

        self.BuildTmpDirectory()
        sCopiedFile = self.CopyChmFileToTempAppPath(sHelpFile)
        self.DecompileHelpFile(sCopiedFile)

    def BuildTmpDirectory(self):
        import os
        import os.path
        sTempAppPath = os.path.join(os.environ['tmp'], 'HelpFileDecompiler')

        if not os.path.isdir(sTempAppPath):
            os.mkdir(sTempAppPath)

    def CopyChmFileToTempAppPath(self, sHelpFile):
        import shutil
        import os.path
        sDestFile = os.path.join(os.environ['tmp'], 'HelpFileDecompiler',
                                    os.path.basename(sHelpFile))
        shutil.copyfile(sHelpFile, sDestFile)
        return sDestFile

    def DecompileHelpFile(self, sHelpFile):
        import win32api
        import subprocess
        import os.path

        if not os.path.isfile(sHelpFile):
            raise Exception("sHelpFile " + sHelpFile + " not found")

        sDecompiledFolder = os.path.join(
            os.environ['tmp'],
            'HelpFileDecompiler',
            os.path.basename(sHelpFile).split(".")[0])

        if not os.path.isdir(sDecompiledFolder):
            os.mkdir(sDecompiledFolder)

        sHELPEXE = "C:\Windows\hh.exe"
        if not os.path.isfile(sHELPEXE):
            raise Exception("sHELPEXE " + sHELPEXE + " not found")

        # Not allowed to quote arguments to HH.EXE
        # so we take the short path to eliminate spaces
        sHelpFileShort = win32api.GetShortPathName(sHelpFile)
        sDecompiledFolderShort = win32api.GetShortPathName(sDecompiledFolder)

        # so now we can run the decompiler
        subprocess.run([sHELPEXE, '-decompile',
                        sDecompiledFolderShort, sHelpFileShort])


if __name__ == '__main__':
    print ("Registering COM servers...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(HelpFileDecompiler)

    helpFile=("C:\\Program Files\\Microsoft Office 15\\root\\vfs\\"
                "ProgramFilesCommonX86\\Microsoft Shared\\VBA\\VBA7.1\\"
                "1033\\VBLR6.chm")
    test = HelpFileDecompiler()
    test.DecompileHelpFileMain(helpFile)

Here is some test VBA client code

Option Explicit

Sub Test()

    Dim obj As Object
    Set obj = VBA.CreateObject("PythonInVBA.HelpFileDecompiler")
    
    obj.DecompileHelpFileMain "C:\Program Files\Microsoft Office 15\root\vfs\" & _
              "ProgramFilesCommonX86\Microsoft Shared\VBA\VBA7.1\" & _
              "1033\VBLR6.chm"

End Sub

Friday, 6 July 2018

OLEDB - C++ - Solved: ATL's OLEDB Provider sample crashes Excel (uncaught exception from msado15.dll)

Happy Conclusion

This post initially did not reach a happy conclusion, I investigated compiling the ATL OLEDB Provider samples but reached an impasse as the ADO runtime throws an exception. I wrote up the problem as a blog post nevertheless because sometimes defeats can be as revealing as victories. I asked on StackOverflow and they came through. Especially Simon Mourier on StackOverflow solved it and I am thankful. I have not changed the main body of the original post, instead I have posted solution at the bottom here.

ADO Recordsets as a marshalling vessel

So I was pondering how to get data from Python to VBA without using COM's SAFEARRAY which can often but not always be acquired with tolist(). One alternative is to create an ADO recordset, it is possible to create an ADO recordset by creating the an Xml representation, i.e. to concatenate a correctly formatted string.

In the era of Visual Basic 5 (upon which VBA is based) code marshalled tabular data from one execution process to another (perhaps even on a different computer as part of an N-tiered scalable architecture, DNA) by transmitting a two dimensional array (SafeArray) housed in OLE Variant. In VB6, the recommended vessel for marshalling tabular became the disconnected ADO recordset. Marshalling means serializing in one process and then deserializing in another. For recordsets, the ADO run-time would do the marshalling where as for OLE Variants it was the COM run-time. They have similar speeds but an ADO recordset packs so much more functionality that one should opt for it.

ADO Recordsets come from OLE DB Providers

The vast majority of ADO recordsets are created by an OLE DB Provider such as the Microsoft SQL Server OLE DB Provider. Can we create our own OLE DB Provider? In theory yes, ATL has had for almost two decades the ability to create OLE DB Providers. I never tried until now. My attempt to get the sample working have got stuck. I'll try not to replicate the documentation, instead I will give links plus some extra info where pertinent.

The ATL Sample OLEDB Provider that Finds Files from a directory

ADO and OLEDB were meant to be an advance on the previous generation of data access technology by allowing a single object model to access not just database tables but also non-tabular data sources such as email stores.

In the case of the ATL Sample OLEDB Provider I have read enough code to say that it (when if works, if ever) actually creates a recordset where each row is a file in a given directory. This is useful as a sample because everyone has a file system and this obviates the need to install a database. It is also illustrative of a non-database source.

The sample reads files by calling API functions such as FindFirstFile.

When running the ATL OLEDB Provider wizard one can customise the name, I will choose "FindFiles".

Running Visual Studio 2017 ATL Wizards to create Find File OLEDB Provider Sample

I will try not to recreate the documentation but will give some screenshots as the official documentation does not (I suspect it is not being maintained).

Because we are creating a COM component you will need to run Visual Studio with admin rights

New Project -> Visual C++ -> ATL -> ATL Project

Click OK on the next screen without entering anything

In the solution explorer you'll find two projects, focus on the top project, FindFiles. On the FindFiles project icon or or any folder icon in the FindFiles project right-click mouse button and then select Add -> New Item . You'll get the following screen from which you should select ATL -> ATL OLEDB Provider and then click Add

The next screen is the wizard, enter FindFiles into the ShortName and (nearly) all the other fields are updated to reflect the short name. Also, add FindFiles to the ProgID, whilst not strictly necessary this will help debugging later. Click finish to commit and the wizard will write the code for you.

Save all the files and compile (don't forget you'll need admin rights).

Enumerating OLE DB Providers

I forget where I found this code (somewhere on Microsoft.com) but it enumerates all the OLE DB Providers installed, after a successful compilation FindFiles should appear

using System;
using System.Data;
using System.Data.OleDb;

namespace OldDbEnumerator
{

    class Program
    {
        static void Main()
        {
            OleDbDataReader reader = OleDbEnumerator.GetRootEnumerator();

            DisplayData(reader);

            Console.WriteLine("Press any key to continue.");
            Console.ReadKey();
        }

        static void DisplayData(OleDbDataReader reader)
        {
            while (reader.Read())
            {
                for (int i = 0; i < reader.FieldCount; i++)
                {
                    Console.WriteLine("{0} = {1}",
                     reader.GetName(i), reader.GetValue(i));
                }
                Console.WriteLine("==================================");
            }
        }
    }
}

And in the output of that is the new Provider, your GUID will be different (it is randomly selected)

SOURCES_NAME = FindFiles Class
SOURCES_PARSENAME = {E387836C-6248-4319-92E8-BCD070844D86}
SOURCES_DESCRIPTION = FindFiles Class
SOURCES_TYPE = 1
SOURCES_ISPARENT = False
SOURCES_CLSID = {E387836C-6248-4319-92E8-BCD070844D86}

Excel VBA client code CRASHES EXCEL

Sorry for caps shouting but it is important to save your Excel session before you play with the next chunk of code. Here is some client VBA code. It crashes Excel VBA.

We'll use for debugging later so give your self a new workbook called TestClient.xlsm, in a new standard module paste the following code

Sub TestOleDbProvider()
    
    On Error GoTo ErrHand
    
    
    Dim cn As ADODB.Connection
    Set cn = New ADODB.Connection
    
    cn.Open "Provider=FindFiles;Server=foo;Database=bar" '* this works
    
    Dim cmd As ADODB.Command
    Set cmd = New ADODB.Command
    
    Set cmd.ActiveConnection = cn   '* this works
    
    cmd.CommandText = "*.*"   '* this works
    
    Stop
    Dim rs As ADODB.Recordset
    Set rs = cmd.Execute  '* crashes here
    
    
    
    Exit Sub
ErrHand:
    Debug.Print Err.Description & " (" & Err.Number & ")"
    'Stop

End Sub

Before you run this code, goto ThisWorkbook and enter the following helpful code which will always navigate to the above code upon workbook opening...

Private Sub Workbook_Open()
    Application.GoTo "TestOleDbProvider"
End Sub

Then save the workbook before you run TestOleDbProvider because it will crash and you will not have an opportunity to save it!

So running the above code crashes Excel, later I will show that an uncaught exception is being thrown by msado15.dll. Now this maybe to do with the fact I do not know how to yet supply a query. Peaking ahead I can show some of the C++ code (CFindFilesRowset::Execute) where it is assumes *.* if supplied an empty string

  CW2TEX<_MAX_PATH> szDir(m_strCommandText == L"" ? L"*.*" : m_strCommandText);

Time to investigate.

Debugging the OLEDB Provider with breakpoints

So we must investigate. I set the project's properties to start Excel and load a workbook called TestClient.xlsm as part of the debug properties. So on the FileFind project icon right-click and select properties (last entry on the menu) to display FindFiles Property Pages within which select the Debugging entry on the left hand side.

Running the code I get an unhandled exception and the call stack is firmly in msado15.dll..

 msado15.dll!CQuery::SetSQL(unsigned short *) Unknown Non-user code. Symbols loaded.
  msado15.dll!CQuery::SetCommandText(long,unsigned long,unsigned char,unsigned char) Unknown Non-user code. Symbols loaded.
  msado15.dll!CQuery::Execute(enum ExecuteTypeEnum,char,unsigned long,bool,unsigned long,unsigned long,long,struct tagVARIANT *,unsigned long,void *,long *,struct _ADORecordset * *) Unknown Non-user code. Symbols loaded.
  msado15.dll!CCommand::_Execute(enum ExecuteTypeEnum,char,unsigned long,bool,unsigned long,unsigned long,long,long,struct tagVARIANT *,unsigned long,void *,long *,struct _ADORecordset * *) Unknown Non-user code. Symbols loaded.
  msado15.dll!CCommand::ExecuteWithModeFlag(struct tagVARIANT *,struct tagVARIANT *,long,struct _ADORecordset * *,int) Unknown Non-user code. Symbols loaded.
  msado15.dll!CCommand::Execute(struct tagVARIANT *,struct tagVARIANT *,long,struct _ADORecordset * *) Unknown Non-user code. Symbols loaded.
  VBE7.DLL!1e813579() Unknown No symbols loaded.
  [Frames below may be incorrect and/or missing, no symbols loaded for VBE7.DLL]  Annotated Frame
  VBE7.DLL!1e7cff4b() Unknown No symbols loaded.
  VBE7.DLL!1e829d13() Unknown No symbols loaded.
  VBE7.DLL!1e82fea2() Unknown No symbols loaded.
  VBE7.DLL!1e82bcb5() Unknown No symbols loaded.
  [External Code]  Annotated Frame

And with that I am stuck. I guess I could ask Stack Overflow. I've never bothered Microsoft for support before, might do so on this occasion. I'll try a SO bounty first I think.

Update: Solution given by Simon Mourier

I am delighted to say that Simon Mourier at StackOverflow solved this by adding another interface ICommandText to the interface map.

BEGIN_COM_MAP(CFindFilesCommand)
    ...
    COM_INTERFACE_ENTRY(ICommandText) 
    ...
END_COM_MAP()

Apparently, the ADO runtime was querying for this interface and not finding it and thus calling on a null pointer. I'm surprised the ADO runtime doesn't check for null pointers but never mind. The cased is solved.

Thursday, 5 July 2018

VBA - Python - OCR - Optical Character Recognition

It was Python month on this blog last month but still plenty of ideas of how to leverage the huge Python ecosystem and bring functionality to the feet of VBA Developers. In this blog I play with Optical Character Recognition (OCR) and get it callable from VBA using a COM gateway class.

Tesseract

The OCR Python library I use here is Tesseract which has a long pedigree and happily has Python bindings. But it needs some care to install properly.

Tesseract Installation

I found that using  pip install pytesseract  falsely reported success. Instead, what was necessary was the following steps...

  1. Find a site with a Tesseract Windows binary installer. I found Tesseract at UB Mannheim
  2. Run the Tesseract Windows binary installer. I ran https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-setup-3.05.02-20180621.exe
  3. Add the Tessearcht directory (for me 'C:\Program Files (x86)\Tesseract-OCR') to PATH environment variable
  4. Close down and restart all potential client processes, Visual Studio, Excel, Command Windows (cmd.exe), Powershell, any process.

We'll also use an Image library, Pillow, to load the image file, so use  pip install pillow 

OCRBatch Com Gateway Class

I have given this pattern many times over the last month here is a Python class with enough extra code to allow it to be registered as a COM class and thus callable from VBA. Here is the code, which must be run at least once under admin privileges to enable registration (thereafter not required).

## pip install pillow      ## succeeded

## https://github.com/UB-Mannheim/tesseract/wiki
## https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-setup-3.05.02-20180621.exe
## add C:\Program Files (x86)\Tesseract-OCR to PATH
## restart Visual Studio so it picks up changes to PATH



class OCRBatch(object):
    _reg_clsid_ = "{A7C1275F-7ABD-4AA2-90E5-462392D821DF}"
    _reg_progid_ = 'PythonInVBA.OCRBatch'
    _public_methods_ = ['RunBatch', 'RunOCR']

    def RunBatch(self, rootDir):
        import os
        for subdir, dirs, files in os.walk(rootDir):
            for file in files:
                #print os.path.join(subdir, file)
                filepath = subdir + os.sep + file

                if file.endswith(".jpeg") and file.startswith("File_"):
                    ocrFile = filepath + ".txt"
                    self.RunOCR (filepath,ocrFile)

    def RunOCR(self,imageFile, ocrFile):
        import io
        from PIL import Image
        import pytesseract

        img = Image.open(imageFile )
        text = pytesseract.image_to_string(img)

        with io.open(ocrFile,'w', encoding="utf-8") as f:
            f.write(text)
            f.close()

if __name__ == '__main__':
    print ("Registering COM servers...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(OCRBatch)

    rootdir="C:\\Users\\Simon\\Downloads\\ocr"
    test = OCRBatch()
    test.RunBatch(rootdir)

The code has two methods RunBatch and RunOCR. The latter operates on a single file whilst the former operates on a folder and its subfolders. The folder is scanned for filenames that start with "File_" and end with ".jpeg" because that is the naming convention of the input device, you should change it to suit your input device.

When run on each file Tesseract will scan the Image and generate some text, the text is then saved to a file in the same directory with very similar name (simply suffixed with ".txt"). For me, the output can be quite jumbled and typically needs cleaning up in a text editor but it is better than typing in from scratch.

Test Script

Just quickly point out that I've added some test script code to the tail of the script, this is where the __main__ procedure runs the COM registration. I find this a useful place to write some test code and will probably continue this convention.

OS Walk

Again, quickly point another (smaller) Python nicety in that os.walk recursively walks a directory structure and its subfolders. In VBA one would need to write some code to do that. Here the Python ecosystem delivers another time saving.

Client VBA Code

So thanks to COM registration the client VBA is trivial...

Option Explicit

Sub Test()

    Dim obj As Object
    Set obj = VBA.CreateObject("PythonInVBA.OCRBatch")
    
    obj.RunBatch "C:\Users\Simon\Downloads\ocr"

End Sub

Node.js - Using HTTPS + CORS

So, on this blog I've given a Node.js simple web service example before but it was plain vanilla and didn't handle either secure connections with HTTPS or handle this curious protocol called CORS (see below). I remedy that here by given some working code which allows POSTing of json payloads.

So to make your Node.js web service handle https you need to use

var https = require('https');
var fs = require('fs');

var options = {
    // create the following two files beforehand with openssl
    // https://stackoverflow.com/questions/12871565/how-to-create-pem-files-for-https-web-server#answer-12907165
    key: fs.readFileSync('key.pem'),
    cert: fs.readFileSync('cert.pem')
};

where you are specifying the private key and the SSL certificate which you must have created before. Switch to your node.js source files directory so openssl.exe creates the files in the right place. A good stack overflow Q&A here shows how to create these files.

CORS Cross-Origin Resource Sharing

Ideally all files and content are delivered from one single web domain (+port). Sometimes a use case is not that simple. Sometimes a web resource needs to be accessed from a different origin. CORS allows this cross origin request. As you can imagine you are opening up a security hole; so in production think carefully about where cross-origin requests might originate and tie down as much as possible. In production, don't use wildcards like in sample code given below! A naughty person could launch a denial of service attack if you allow them! A good CORS guide is here.

Pre-flight request

Today I learnt that some client will send an HTTP OPTIONS request before sending a HTTP POST request; this is known as a "pre-flight request". If you are not using a web server framework then you will need to handle this manually by setting HTTP response headers like the following...

            // IN PRODUCTION DO NOT USE WILDCARDS!!!
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
            response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, content-type, Accept");

So in the above code I have not tied down Origin but you dear reader must do in production! But this is a blog sample. I have tied down the HTTP methods, only allowing POST and OPTIONS. I have also tied down the headers.

Whilst one should should tie down options as much as possible if you tie down too much your client will assume it has been refused permission and not send the follow-on POST request (in my use case). I had to debug CORS today because my POSTs were not coming through. If the preflight OPTIONS request is not implemented then yes any POST request will not follow on.

Anyway, here is a full working example. The following code runs an HTTPS service at 127.0.0.1:8000 and only allows OPTIONS and GET. Don't forget in production to further restrict the origin and not use wildcards...

'use strict';

var https = require('https');
var fs = require('fs');

var options = {
    // create the following two files beforehand with openssl
    // https://stackoverflow.com/questions/12871565/how-to-create-pem-files-for-https-web-server#answer-12907165
    key: fs.readFileSync('key.pem'),
    cert: fs.readFileSync('cert.pem')
};

const port = 8000;

console.log('nversion 0.001n');

//https://stackoverflow.com/questions/5998694/how-to-create-an-https-server-in-node-js#answer-21809393
https.createServer(options, function (request, response) {

    switch (request.method) {
        case "OPTIONS":
            // IN PRODUCTION DO NOT USE WILDCARDS!!!
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
            response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, content-type, Accept");
            response.end();
            break;
        case "GET":
            console.log(request.url);
            response.end('Hello Node.js Server!');
            break;
        case "POST":
            // IN PRODUCTION DO NOT USE WILDCARDS!!!
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
            response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, content-type, Accept");
            
            //https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/

            let body = [];
            request.on('data', (chunk) => {
                body.push(chunk);
            }).on('end', () => {
                body = Buffer.concat(body).toString();
                // at this point, `body` has the entire request body stored in it as a string
                console.log('nbody received:nn' + body);

            });

            response.end();
            break;
    }
}).listen(port);

Excel - VBA - Parsing International Currency Amounts in VBA

VBA Currency Parsing is limited to one's own currency

So parsing currencies in ordinary VBA can work so long as one sticks to one's own currency, so the following, CCur can handle commas and minus signs and on my machine Sterling Pound symbol but trips up on dollars.

Sub Test()

    Debug.Assert VBAParseCCur("1000") = 1000
    Debug.Assert VBAParseCCur("-1000") = -1000
    Debug.Assert VBAParseCCur("-10,000") = -10000
    
    Debug.Assert VBAParseCCur("£-10,000") = -10000 '* works on my (UK) machine
    Debug.Assert VBAParseCCur("$-10,000") = -10000  '* fails on my (UK) machine

End Sub

Function VBAParseCCur(v)
    On Error Resume Next
    VBAParseCCur = CCur(v)
End Function

COM System Parsers

So typically C++ programmers have greater access to the system functions than VBA programmers. A C++ programmer can call VarParseNumFromStr to parse a number out of a string.

HRESULT VarParseNumFromStr(
  LPCOLESTR strIn,
  LCID      lcid,
  ULONG     dwFlags,
  NUMPARSE  *pnumprs,
  BYTE      *rgbDig
);

One can see that one can specify the locale id as second parameter. If you know what you want, in this case I want a currency from a string then you can chose a more specific parsing function, VarCyFromStr

HRESULT VarCyFromStr(
  LPCOLESTR strIn,
  LCID      lcid,
  ULONG     dwFlags,
  CY        *pcyOut
);

With the help of VBFormus.com expert Olaf Schmidt we can call VarCyFromStr from VBA. And now we have the ability to parse multi-currency strings, see this code.

Option Explicit


'* http://www.vbforums.com/showthread.php?762443-VB6-Tabulator-Crosstab-Class
'* https://docs.microsoft.com/en-gb/previous-versions/windows/desktop/api/oleauto/nf-oleauto-varcyfromstr
Private Declare Function VarCyFromStr& Lib "oleaut32" (ByVal sDate&, ByVal LCID&, ByVal Flags&, C As Currency)

Private Enum lcidLocaleId
    lcidEN_US = 1033    '* US
    lcidEN_EN = 2057    '* UK
    lcidFR_FR = 1036    '* Eurozone
    lcidRU = 1049       '* Russia
    lcidJA = 1041       '* Japan
End Enum

Private Enum chrwCurrencies
    chrwEuro = 8364     '* for euros
    chrwRouble = 8381   '* for roubles
    chrwYen = 165       '* for yen
End Enum

Private Function CCurLA(ByVal sAmount As String, ByVal LCID As lcidLocaleId) As Currency
    Dim HRes As Long
    
    HRes = VarCyFromStr(StrPtr(sAmount), LCID, 0, CCurLA)
    If HRes Then Err.Raise HRes
End Function

Sub TestVarCyFromStr_Yen()

    Dim sYen As String
    sYen = ChrW(chrwYen) & "-1000"
    Dim curYen As Currency
    
    curYen = CCurLA(sYen, lcidJA)
    Debug.Assert curYen = -1000
End Sub

Sub TestVarCyFromStr_Roubles()

    Dim sRoubles As String
    sRoubles = ChrW(chrwRouble) & "-1000"
    Dim curRoubles As Currency
    
    curRoubles = CCurLA(sRoubles, lcidRU)
    Debug.Assert curRoubles = -1000
End Sub

Sub TestVarCyFromStr_Euros()

    Dim sEuros As String
    sEuros = ChrW(chrwEuro) & "-1000"
    Dim curEuros As Currency
    
    curEuros = CCurLA(sEuros, lcidFR_FR)
    Debug.Assert curEuros = -1000
End Sub

Sub TestVarCyFromStr_Dollars()

    Dim sDollars As String
    sDollars = "$-10,00"
    Dim curDollars As Currency
    
    curDollars = CCurLA(sDollars, lcidEN_US)
    Debug.Assert curDollars = -1000
End Sub

Sub TestVarCyFromStr_Pounds()

    Dim sPounds As String
    sPounds = "£-10,00"
    Dim curPounds As Currency
    
    curPounds = CCurLA(sPounds, lcidEN_EN)
    Debug.Assert curPounds = -1000
End Sub

Python - VBA - Com Interop - Factory Method Pattern

Python can interop with VBA via COM. One can create a Python class and expose it to VBA for scripting, this allows VBA developers to access the massive Python ecosystem. Sometimes, if you want to build an object hierarchy then you want some classes to be only creatable via another class, this is sometimes called the Factory Method Pattern. It can frequently occur in parent-child object relationships. In this post I show how to do this in Python.

Here is the Python code which must be run at least once with admin privilege to ensure COM registration (thereafter not required).

import win32com.client


class MyParent(object):
    _reg_clsid_ = "{C61A7C6E-B657-4D55-AD36-8850B2E501AC}"
    _reg_progid_ = 'PythonInVBA.MyParent'
    _public_methods_ = ['Greet', 'GetChild']

    def __init__(self):  # Rules of Com say parameterless constructors
        self.child = win32com.client.Dispatch("PythonInVBA.MyChild")
        self.child.SetName("foo")

    def Greet(self):
        return "Hello world"

    def GetChild(self):
        return self.child


class MyChild(object):
    _reg_clsid_ = "{15DAAEE2-3A37-4DE1-9973-CCD011DF4888}"
    _reg_progid_ = 'PythonInVBA.MyChild'
    _public_methods_ = ['Initialize', 'GetName', 'SetName']

    def __init__(self):  # Rules of Com say paramerless constructors
        pass

    def GetName(self):
        return self.name

    def SetName(self, sName):
        self.name = sName

if __name__ == '__main__':
    print ("Registering COM servers...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(MyParent)
    win32com.server.register.UseCommandLine(MyChild)

The trick is to create the child class with win32com.client.Dispatch("PythonInVBA.MyChild") and not the standard constructor MyChild() . My thanks to Stackoverflow user Kunif for solving this.

And so the VBA client code looks like this

Sub Test_MyParent_Returning_MyChild()
    On Error GoTo ErrHand:

    Dim objMyParent As Object
    Set objMyParent = VBA.CreateObject("PythonInVBA.MyParent")

    Dim objMyChild As Object

    Set objMyChild = objMyParent.GetChild()
    Debug.Print objMyChild.GetName  '* prints foo

    Exit Sub
ErrHand:
    Debug.Print Err.Description
End Sub

Saturday, 30 June 2018

Python - Some month end observations...

Final Thoughts

I have thoroughly enjoyed programming in Python this month. I have been surprised by the huge ecosystem of free packages that is available. Any VBA developer who is frustrated not to find a helpful COM library for a particular task should definitely investigate the Python ecosystem as chances are someone has already written it.

I didn't complete everything I wanted. I will continue to work through the agenda in my head. In fact Python will now definitely feature on this blog regularly alongside VBA, Javascript and others.

Miscellany

A miscellany of thoughts not substantial enough to warrant their own blog posts

VBA Programmers should stop Shelling and use the Gateway Class design pattern instead

Perusing StackOverflow it seems very many devs are using Shell to run Python when they could be using the Gateway class design pattern, plnety of examples on this blog this month.

VBA Programmers should stop using Python with the Script Control and use the Gateway Class design pattern instead

I have used the ScriptControl for Javascript and have had some success calling Javascript features from VBA (such as parsing JSON). So I know ScriptControl is not good for debugging. I'd use Visual Studio to debug and I'd use a Gateway class to invoke the exact same script file instead of having to adjust the Python code for injection into ScriptControl. Besides I think money is required to run Python on the ScriptControl in production. Whereas the gateway class approach is free.

Python is a script but it is compiled

One can see __pychache__ directories containing *.pyc files which may not be machine code but are in fact byte code. This is reassuring and ought to aid performance for a script technology.

Python has a virtual machine

According to this SO answer

Python Bootcamps

Just some links to some Python bootcamp listings

Python - Excel - Call Python from worksheet using reflection

Today is final day of Python month on this Excel Development Platform blog where I highlight some Python technologies of interest to Excel (VBA) Developers.

Here I show how one can call Python functions from a worksheet. It requires a VBA pass through function but don't worry we can very easily use reflection to very easily automatically generate the VBA pass through functions.

Python Calculator with Reflection

So I have shown the pattern in the code below many times this month where I define a class and register the COM class so it is creatable and callable from VBA. It is a requirement to maintain a list of public methods for the COM interop to work. We can use this list to filter reflection artefacts.

In the code below pythonSum and pythonMultiply are just example methods which we will call from the worksheet. They are named distinctly to help distinguish from other worksheet functions; you can choose your own naming convention.

What's new in the code below is the reflect method. This method uses Python's inspect module. First, we call inspect.gemembers() but we have to filter these results firstly against our list of _public_methods_ and also to filter out the reflect method itself; this yields a collection of two methods pythonSum and pythonMultiply. Then for both of these methods we call inspect.getargspec() but from the results we remove the leading self argument and keep the remaining arguments.

The method name and argument lists are then packed into a Scripting.Dictionary for consumption in VBA.

class PythonCalculator(object):
    _reg_clsid_ = "{38C05F9C-5FEA-40A3-83B3-FD0E102E97B7}"
    _reg_progid_= 'PythonInVBA.PythonCalculator'
    _public_methods_ = ['pythonMultiply','pythonSum','reflect']

    def reflect(self):
        import inspect 
        import win32com.client
        msDict = win32com.client.Dispatch("Scripting.Dictionary")
        
        for inspectMethodLoop in inspect.getmembers(self):
            if inspectMethodLoop[0] in PythonCalculator._public_methods_:
                if inspectMethodLoop[0]!='reflect':
                    msDict.add(inspectMethodLoop[0],str(inspect.getargspec(inspectMethodLoop[1]).args[1:]))

        return msDict 

    def pythonSum(self,x,y):
        return x+y

    def pythonMultiply(self,a,b):
        return a*b

if __name__=='__main__':
    print ("Registering COM server...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(PythonCalculator)

VBA Code to Auto-generate Pass-Through Worksheet Functions

So on the VBA side we write the following code which calls the reflect method and then processes the results in the Scripting.Dictionary and automatically generates VBA source code for the pass through functions which are then callable from a worksheet. Here is the VBA code...

Sub Test4()
    On Error GoTo ErrHand:

    Dim obj As Object
    Set obj = VBA.CreateObject("PythonInVBA.PythonCalculator")
    
    Dim dictReflect As Object
    Set dictReflect = obj.reflect()
    
    Debug.Print Reflect2("PythonInVBA.PythonCalculator", dictReflect)
    
SingleExit:
    Exit Sub

ErrHand:
    Debug.Print Err.Description
    GoTo SingleExit
End Sub


Function Reflect2(ByVal sClassName As String, ByVal dictReflect As Object)
    Dim sVBACode As String
    
    Dim vKeyLoop As Variant
    For Each vKeyLoop In dictReflect
        Dim sArgsInRoundBrackets As String
        sArgsInRoundBrackets = VBA.Replace(VBA.Replace(VBA.Replace(dictReflect(vKeyLoop), "'", ""), "[", "("), "]", ")")
        Dim sFunc As String
        sFunc = "Function " & vKeyLoop & sArgsInRoundBrackets & vbNewLine
        
        sFunc = sFunc & vbTab & vKeyLoop & " = VBA.CreateObject(""" & sClassName & """)." & vKeyLoop & sArgsInRoundBrackets & vbNewLine
        
        sFunc = sFunc & "End Function" & vbNewLine & vbNewLine
        sVBACode = sVBACode & sFunc
    Next

    Reflect2 = sVBACode
End Function

The output of the above code is some more VBA code sitting in the Immediate window which you should then copy and paste into your source. Here is the output.

Function pythonMultiply(a, b)
    pythonMultiply = VBA.CreateObject("PythonInVBA.PythonCalculator").pythonMultiply(a, b)
End Function

Function pythonSum(x, y)
    pythonSum = VBA.CreateObject("PythonInVBA.PythonCalculator").pythonSum(x, y)
End Function

After pasting the pass through functions into your source the functions are now callable from the worksheet. As proof that this works here is a little screenshot of a worksheet showing the pythonMultiply() function being called.

Final Thoughts

I have seen some Python packages that go to great lengths to make Python callable from the worksheet but this is unnecessary. A trivial amount of VBA that can be very easily generated with reflection gets you there. This took less than an hour to figure out.

Friday, 29 June 2018

Python - Pandas - ADO - Convert a Pandas DataFrame to an ADO Recordset

So it's Python month on this Excel Development Platform blog where I highlight some Python technologies of interest to Excel (VBA) Developers.

So Python has the Pandas data processing library and one could move logic from VBA into a Python middle tier application server but sometimes you may still want some data processing functionality to remain in the VBA layer. Can a Pandas DataFrame be converted to an ADO Recordset? Yes, but you'll need to convert it into an Xml representation first and then pass the string to VBA where it recreates the ADO recordset.

Python code to convert Pandas dataframe to Xml representation of an ADO Recordset

Much of the Xml representation of an ADO recordset is boilerplate code, however in the first section one can see the column names of Col1,Col2,Col3. Then in the z:row elements the field values are added as attributes and the attribute names must match the column names Col1,Col2,Col3. Then add a tail and return the whole string to VBA.

import pandas as pd
import numpy as np

class PopulationDensity(object):
    _reg_clsid_ = "{C50910CC-F88F-4EA5-86D4-1E5D6AF1F4AE}"
    _reg_progid_= 'PandasInVBA.PopulationDensity'
    _public_methods_ = ['getPivotTable','getADORecordset']

    def getADORecordset(self):
        url="https://raw.githubusercontent.com/datasets/house-prices-uk/master/data/data.csv"
        whole=pd.read_csv(url)

        ## project first three columns
        projected = whole[['Date','Price (All)','Change (All)']]

        ## So now start creating the Xml representation of an ADO Recordset
        sStart = ("<xml xmlns:x='urn:schemas-microsoft-com:office:excel' "  +
        "    xmlns:dt='uuid:C2F41010-65B3-11d1-A29F-00AA00C14882' "  +
        "    xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882' "  +
        "    xmlns:rs='urn:schemas-microsoft-com:rowset' "  +
        "    xmlns:z='#RowsetSchema'>"  +
        "<x:PivotCache>"  +
        "<x:CacheIndex>1</x:CacheIndex>"  +
        "<s:Schema id='RowsetSchema'>"  +
        "<s:ElementType name='row' content='eltOnly'>"  +
        "<s:attribute type='Col1'/>"  +
        "<s:attribute type='Col2'/>"  +
        "<s:attribute type='Col3'/>"  +
        "<s:extends type='rs:rowbase'/>"  +
        "</s:ElementType>"  +
        "<s:AttributeType name='Col1' rs:name='Date'>"  +
        "<s:datatype dt:maxLength='255'/>"  +
        "</s:AttributeType>"  +
        "<s:AttributeType name='Col2' rs:name='Price (All)'>"  +
        "<s:datatype dt:maxLength='255'/>"  +
        "</s:AttributeType>"  +
        "<s:AttributeType name='Col3' rs:name='Change (All)'>"  +
        "<s:datatype dt:maxLength='255'/>"  +
        "</s:AttributeType>"  +
        "</s:Schema>"  +
        "<rs:data>" )

        ## now the data section, we iterate over the rows of the pandas DataFrame
        sData =""
        for index, row in projected.iterrows():
            sData = sData + "<z:row Col1='" + str(row['Date']) + "' Col2='" + str(row['Price (All)']) + "' Col3='" + str(row['Change (All)']) + "'/>"

        sEnd = (
        "</rs:data>"  +
        "</x:PivotCache>"  +
        "</xml>"  )

        return sStart + sData + sEnd 

    def getPivotTable(self):
        pass  # see previous article

if __name__=='__main__':
    print ("Registering COM server...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(PopulationDensity)

Client VBA Code

So here is the VBA code. An Xml Dom document is created, and the string returned from Python is parsed as a document. Then an ADO recordset is created and we call Open passing the DomDocument as the argument. This technique leverages the fact that recordsets can be persisted to xml files.

Sub Test2()

    Dim obj As Object
    Set obj = VBA.CreateObject("PandasInVBA.PopulationDensity")
    
    Dim sDataAsXml As String
    sDataAsXml = obj.getADORecordset
    
    '* Tools->References:Microsoft Xml, v6.0
    Dim domXlPersist As MSXML2.DOMDocument60
    Set domXlPersist = New MSXML2.DOMDocument60
    domXlPersist.LoadXML sDataAsXml
    Debug.Assert domXlPersist.parseError.ErrorCode = 0
    
    '* Tools->References:Microsoft ActiveX Data Object 6.1 Library
    Dim rs As ADODB.Recordset
    Set rs = New ADODB.Recordset
    rs.Open domXlPersist
    

    Dim rngOrigin As Excel.Range
    Set rngOrigin = Sheet3.Cells(12, 1)
    
    '* write column headers
    Dim lFieldLoop As Long
    For lFieldLoop = 0 To rs.Fields.Count - 1
        rngOrigin.Offset(0, lFieldLoop).Value = rs.Fields.Item(lFieldLoop).Name
    Next lFieldLoop
    
    '* write the data, yes, in one line
    rngOrigin.Offset(1, 0).CopyFromRecordset rs

End Sub

Final Thoughts

In the code given I have serialized a Dataframe to a (potentially large) Xml string then on the client side parsed it into a Dom and then an ADO recordset. This is quite a heavy set of operations. If you are calling an in process component then it would be better to pass it back as an OLE Variant. However, the above technique maybe better suited for Flask web services where conversion to strings is standard practice as part of the HTTP protocol.

Python - Pandas - VBA - Return a Pandas Pivot Table to VBA (using COM)

So it's Python month on this Excel Development Platform blog where I highlight some Python technologies of interest to Excel (VBA) Developers.

A few days prior I blogged a post about using Pandas to process some very large Eurostat Population Density data set, once I got the Pandas pivot table I saved it to a csv file which is unsatisfactory; the truth is I was having difficulty returning the Pandas pivot table to VBA as a 2d OLE Variant that can be pasted onto a worksheet. In this post I fix that.

Python code to return an OLE Variant from a Pandas Pivot

First the Python code. So the upper half of this code is the same as in the previous pandas article. So this task involves extracting portions of the pivot table, converting them to numpy arrays and then glueing them into one large 2d array.

import pandas as pd
import numpy as np

class PopulationDensity(object):
    #"{C50910CC-F88F-4EA5-86D4-1E5D6AF1F4AE}"
    _reg_clsid_ = "{C50910CC-F88F-4EA5-86D4-1E5D6AF1F4AE}"
    _reg_progid_= 'PandasInVBA.PopulationDensity'
    _public_methods_ = ['getPivotTable']

    def getPivotTable(self):
        ## csv file in zip http://ec.europa.eu/eurostat/cache/GISCO/geodatafiles/GEOSTAT-grid-POP-1K-2011-V2-0-1.zip

        url="C:/Users/Simon/Downloads/GEOSTAT-grid-POP-1K-2011-V2-0-1/Version 2_0_1/GEOSTAT_grid_POP_1K_2011_V2_0_1.csv"
        whole=pd.read_csv(url, low_memory=False)

        ## only want two columns
        populationDensity=whole[['TOT_P','CNTR_CODE']]

        ## trying to replicate graph here http://www.centreforcities.org/wp-content/uploads/2018/04/18-04-16-Square-kilometre-units-of-land-by-population.png
        ## which aggregates the records by brackets

        # https://stackoverflow.com/questions/25010215/pandas-groupby-how-to-compute-counts-in-ranges#answer-25010952
        # also relevant https://stackoverflow.com/questions/21441259/pandas-groupby-range-of-values#answer-21441621
        ranges = [0,10000,15000,20000,25000,30000,35000,40000,45000,1000000]

        ## Yes groupby but we want to work with a DataFrame afterwards
        # https://stackoverflow.com/questions/10373660/converting-a-pandas-groupby-object-to-dataframe
        groupedByBracketandCountry = populationDensity.groupby(['CNTR_CODE',pd.cut(populationDensity['TOT_P'],ranges).astype(str)]).size().to_frame(name = 'count').reset_index()

        # https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.pivot_table.html#pandas.DataFrame.pivot_table
        pivottable2=pd.pivot_table(groupedByBracketandCountry, fill_value=0, values='count', index='TOT_P',columns=['CNTR_CODE'] , aggfunc=np.sum)

        # new code here that extracts portions of pivot table into arrays and then glues then together into one returnable array

        pivottable2Values = pivottable2.values             # this gives the data elements of the pivot table
        pivottable2Columns = pivottable2.columns.values    # this gives the column headers of the pivot table

        # the next line glues one 2d array on top of another (i.e. vertically)
        pivottable2ColumnsAndValues = np.vstack((pivottable2Columns,pivottable2Values))

        # this will be our origin cell, in the top left
        pythontopLeft=np.zeros((1,1))

        # this gets our rows headers in a 2d array, np.reshape upgrades the 1d array to 2d
        indexValues =  np.reshape(pivottable2.index.values, (-1, 1))

        # this next line glues one 2d array on top of another (i.e. vertically)
        pivottable2Index = np.vstack((pythontopLeft,indexValues))

        # this next line glues one 2d array onto the side of another (i.e. horizontally)
        vRet = np.hstack((pivottable2Index,pivottable2ColumnsAndValues))

        # this final line helps the conversion to an OLE Variant pastable onto an Excel worksheet
        return vRet.tolist()

if __name__=='__main__':
    print ("Registering COM server...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(PopulationDensity)

np.vstack, np.hstack and np.reshape

There are some interesting numpy array functions introduced here np.vstack, np.hstack and np.reshape; the first two glue two array either vertically or horizontally; np.reshape can be used to redimension an array.

Without tolist() you get this error

The final requirement to pass back a numpy array to Excel VBA is to use tolist() because numpy array are not convertible to an OLE Variant but lists are. If you forget to add to tolist() then you get the following error.

Unexpected Python Error: ValueError: ndarray is not C-contiguous

VBA Client Code

So now the VBA is quite simple. The Python returns an OLE Variant array of two dimensions, we calculate the dimensions and then paste onto the sheet.

Sub Test()

    Dim obj As Object
    Set obj = VBA.CreateObject("PandasInVBA.PopulationDensity")
    
    Dim v
    v = obj.getPivotTable
    
    Dim lRows As Long
    lRows = UBound(v, 1) - LBound(v, 1)
    
    Dim lColumns As Long
    lColumns = UBound(v, 2) - LBound(v, 2)
    
    Dim rng As Excel.Range
    Set rng = Sheet3.Range(Sheet3.Cells(1, 1), Sheet3.Cells(lRows, lColumns))
    rng.Value = v
End Sub

Python - Excel - Scripting Excel Objects and Files

So it's Python month on this Excel Development Platform blog where I showcase interesting Python technologies that might be of interest to Excel (VBA) Developers. Python does COM interop so it can script against the Excel Object Model. So what are the options?

Python to replace VBA?

Your first option could be to wait because VBA is old. VBA is as dated as Visual Basic 6.0 (they are essentially the same language). At some point VBA will be replaced. We do not know if VBA will be replaced with C# or VB.NET. YCombinator reports Excel team considering Python as scripting language, asking for feedback. Here is the feedback forum.

If Microsoft does replace VBA with Python I should imagine Microsoft would do a pretty good job of it.

Python COM Interop and Excel, win32com

So VBA scripts against the Excel object model. In fact the Excel object model is exported and exposed to any COM enabled language. Python has COM interop packages and is thus COM enabled. The key package is win32com. Below is some sample code which should be easy to follow for VBA programmers familiar with the Excel object model. The code is taken from Tim Golden's excellent website. Tim Golden has all sorts of excellent Python for windows tips here. I'd like to particularly highlight folder watching which I could use as part of a message queue solution. They leverage the pywin32 library written by Mark Hammond (twitter github ), another superb technologist!

import os, sys  #Written by Tim Golden
import csv
import tempfile
import win32com.client

#
# Cheating slightly because I know a .csv
# file will register as an Excel object
#

filename = tempfile.mktemp (".csv")
f = open (filename, "wb")
writer = csv.writer (f)
writer.writerows (range (i, i+5) for i in range (10))
f.close ()

#
# NB filename must (in effect) be absolute
#
os.startfile (filename)

wb1 = win32com.client.GetObject (filename)
wb1_c11 = wb1.ActiveSheet.Cells (1, 1)
print wb1_c11.Value
wb1_c11.Value += 1

xl = win32com.client.GetObject (Class="Excel.Application")
# could also use:
# xl = win32com.client.GetActiveObject ("Excel.Application")
xl_c11 = xl.ActiveSheet.Cells (1, 1)
print xl_c11.Value
xl_c11.Value += 1

print wb1_c11.Value
print xl_c11.Value

wb1.Close ()

os.remove (filename)

Python Packages Without Excel.exe

The following packages do not need a running copy of Excel. The authors have very kindly wrestled with the Excel file format specifications and put together code which can either create, read or manipulate Excel files without needing to run a copy of Excel. Economical for Excel licences and also ensures code can run on Linux (where Excel.exe cannot run) for further cost savings.

openpyxl

The documentation states

Openpyxl is a Python library for reading and writing Excel 2010 xlsx/xlsm/xltx/xltm files. It was born from lack of existing library to read/write natively from Python the Office Open XML format.

So this refers the face that Office 2010 introduced new file type options, xlsx and xlsm which are Xml based (and then zipped). To be fair the Xml formats are hard to follow and they have done us a great favour in writing this library. Here is some sample code to get a flavour.

from openpyxl import Workbook
wb = Workbook()

# grab the active worksheet
ws = wb.active

# Data can be assigned directly to cells
ws['A1'] = 42

# Rows can also be appended
ws.append([1, 2, 3])

# Python types will automatically be converted
import datetime
ws['A2'] = datetime.datetime.now()

# Save the file
wb.save("sample.xlsx")

xlsxwriter

Another package written for xlsx (Xml) file types this time written by Jon McNamara (github, twitter). John McNamara is also on cpan.org where has prolifically written many packages in different languages including Lua, Python, Ruby, C and Perl. Here is some sample code.

##############################################################################
#
# A simple example of some of the features of the XlsxWriter Python module.
#
# Copyright 2013-2018, John McNamara, jmcnamara@cpan.org
#
import xlsxwriter


# Create an new Excel file and add a worksheet.
workbook = xlsxwriter.Workbook('demo.xlsx')
worksheet = workbook.add_worksheet()

# Widen the first column to make the text clearer.
worksheet.set_column('A:A', 20)

# Add a bold format to use to highlight cells.
bold = workbook.add_format({'bold': True})

# Write some simple text.
worksheet.write('A1', 'Hello')

# Text with formatting.
worksheet.write('A2', 'World', bold)

# Write some numbers, with row/column notation.
worksheet.write(2, 0, 123)
worksheet.write(3, 0, 123.456)

# Insert an image.
worksheet.insert_image('B5', 'logo.png')

workbook.close()

xlrd

The documentation says

xlrd is a library for reading data and formatting information from Excel files, whether they are .xls or .xlsx files.

So this package can read older formats as well as the newer .xlsx .xlsm (Xml) formats.

xlutils

The documentation describes this as

This package provides a collection of utilities for working with Excel files.

Without delving through all of the documentation I think this library is more file stream based, a bit like SAX for Xml. Here is some sample code defining a filter that allows even rows through but odd rows not ...

>>> from xlutils.filter import BaseFilter
>>> class EvenFilter(BaseFilter):
...
...     def row(self,rdrowx,wtrowx):
...         if not rdrowx%2:
...             self.next.row(rdrowx,wtrowx)
...
...     def cell(self,rdrowx,rdcolx,wtrowx,wtcolx):
...         if not rdrowx%2:
...             self.next.cell(rdrowx,rdcolx,wtrowx,wtcolx)

And here is run through using the filter give above...

>>> from xlutils.filter import process
>>> process(
...     MyReader('test.xls'),
...     MyFilter('before'),
...     EvenFilter(),
...     MyFilter('after'),
...     MyWriter()
...     )
before start
after start
before workbook  test.xls
after workbook  test.xls
before sheet  Sheet1
after sheet  Sheet1
before row 0 0
after row 0 0
before cell 0 0 0 0
after cell 0 0 0 0
before cell 0 1 0 1
after cell 0 1 0 1
before row 1 1
before cell 1 0 1 0
before cell 1 1 1 1
before sheet  Sheet2
after sheet  Sheet2
before row 0 0
after row 0 0
before cell 0 0 0 0
after cell 0 0 0 0
before cell 0 1 0 1
after cell 0 1 0 1
before row 1 1
before cell 1 0 1 0
before cell 1 1 1 1
before finish
after finish

Wednesday, 27 June 2018

Python - Web - Visual Studio 2017 - Project Templates Surveyed

So this is Python month on this blog where I review Python technologies that might be of interest to Excel (VBA) developers. Web services are a way of exporting functionality to a wide range of clients including Excel VBA. So on the client side, VBA can call web services using the MSXML2.XMLHTTPRequest (XHR) class. On the server side can be any technology so long as it supports HTTP protocols.

VS2017 Python Web Project Templates Surveyed

In this post I look at the Visual Studio 2017 project templates catagorized as 'Python - Web' to see which is best for building a web service consumable from Excel VBA.

VS2017 Python Web Project Templates' Summaries

In Visual Studio 2017 when one select a new Project on has the following choices, selecting each one in turn one an read the template summary, I have deposited them here ...

Azure Cloud Service A project for creating a scalable service that runs on Microsoft Azure
Web Project A project for creating a generic Python web project
Bottle Web Project A project for creating an application using the Bottle web framework. It features sample pages that use the Twitter Bootstrap framework for responsive web design.
Django Web project A project for creating an application using the Django web framework. It features sample pages that use the Twitter Bootstrap framework for responsive web design.
Flask Web Project A project for creating an application using the Flask web framework with the Jinja template engine. It features sample pages that use the Twitter Bootstrap framework for responsive web design.
Flask/Jade Web Project A project for creating an application using the Flask web framework with the Jade template engine. It features sample pages that use the Twitter Bootstrap framework for responsive web design.
Blank Bottle Web Project A project for creating a Bottle web project.
Blank Django Web Project A project for creating a Django project.
Blank Flask Web Project A project for creating a Flask web project.
Polls Bottle Web Project A sample polls application using the Bottle web framework. It can be configured to use Azure Table Storage or MongoDB for storage.
Polls Django Web Project A sample polls application using the Django web framework.
Polls Flask Web Project A sample polls application using the Flask web framework. It can be configured to use Azure Table Storage or MongoDB for storage.
Polls Flask/Jade Web Project A sample polls application using the Flask web framework with the Jade template engine. It can be configured to use Azure Table Storage or MongoDB for storage.

VS2017 Python Web Project Templates' Feature Grid

It is probably better to see the difference between the templates with a feature grid ...

Blank Sample App (Polls) Framework Template Engine Bootstrap samples Azure Table Storage/MongoDB storage
Web Project
Bottle Web Project Bottle x
Django Web project Django x
Flask Web Project Flask Jinja x
Flask/Jade Web Project Flask Jade x
Blank Bottle Web Project x Bottle
Blank Django Web Project x Django
Blank Flask Web Project x Flask
Polls Bottle Web Project x Bottle x
Polls Django Web Project x Django x
Polls Flask Web Project x Flask x
Polls Flask/Jade Web Project x Flask Jade x

VS2017 Python Web Project Templates Glossary

And we ought to provide a glossary and links for some of those terms ....

BottleWikipedia: Bottle is a WSGI micro web-framework for the Python programming language.
DjangoWikipedia: Django is a free and open-source web framework, written in Python, which follows the model-view-template (MVT) architectural pattern
FlaskWikipedia: Flask is a micro web framework written in Python. It is classified as a microframework because it does not require particular tools or libraries.
JinjaWikipedia: Jinja is a template engine for the Python programming language and is licensed under a BSD License created by Armin Ronacher.
JadeRenamed Pug. Jade/Pug is an HTML template engine
BootstrapWikipedia: Bootstrap is a free and open-source front-end framework (library) for designing websites and web applications.
Azure Table StorageMicrosoft: A NoSQL key-value store for rapid development using massive semi-structured datasets.
MongoDB storageWikipedia: MongoDB is a free and open-source cross-platform document-oriented database program.
WSGIWikipedia: The Web Server Gateway Interface (WSGI) is a simple calling convention for web servers to forward requests to web applications or frameworks written in the Python programming language.

So Django is for a fully fledged web front end. Both Flask and Bottle are "microframeworks" which are lightweight compared to Django. Flask and Bottle seem quite similarly pitched, so which to choose? Well (at the time of writing) Flask has 22,409 StackOverflow questions and Bottle only has 1249 StackOverflow questions, making one metric in favour of Flask. One can review the battle of these frameworks with Google.

Blank Flask Web Project Web Service

So to see what one gets out of the box, selecting Blank Flask Web Project does some installs and gets a source file app.py which looks like this...

"""
This script runs the application using a development server.
It contains the definition of routes and views for the application.
"""

from flask import Flask
app = Flask(__name__)

# Make the WSGI interface available at the top level so wfastcgi can get it.
wsgi_app = app.wsgi_app


@app.route('/')
def hello():
    """Renders a sample page."""
    return "Hello World!"

if __name__ == '__main__':
    import os
    HOST = os.environ.get('SERVER_HOST', 'localhost')
    try:
        PORT= 51382 #PORT = int(os.environ.get('SERVER_PORT', '5555'))
    except ValueError:
        PORT = 5555
    app.run(HOST, PORT)

Note, i have fixed the port number to be 51382. Running the project runs the Flask app in a development server ...

 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://localhost:51382/ (Press CTRL+C to quit)
127.0.0.1 - - [27/Jun/2018 18:21:14] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [27/Jun/2018 18:21:14] "GET /favicon.ico HTTP/1.1" 404 -

and a new browser windows is spawned and navigates to the app's web address. The app is also callable from Excel VBA...

Sub TestCallFlask()
    '* Tools->References->Microsoft XML, v6.0
    Dim oXHR As MSXML2.XMLHTTP60
    Set oXHR = New MSXML2.XMLHTTP60
    
    oXHR.Open "GET", "http://localhost:51382/", False
    oXHR.send
    
    Debug.Print oXHR.responseText 'Prints Hello World!

End Sub

Links

Final Thoughts

Earlier examples in Python month show VBA<-->Python interop via a COM class but some return types (such as pandas' DataFrame) don't easily convert to something returnable through these classes (they often have to be convertible to a SAFEARRAY). Shipping your app as a web service can help bridge interop difficulties so web services are a valuable option.