(Source: personal experience. I work on a Python IDE that has to deal with lots of that kind of stuff for e.g. implementation of refactoring. Once we had our whitespace-preserving AST, the rest was and remains easy.)
You have different use case, which is 100% controlled code. IOW, you can rely on the original indentation to create the AST.
My use case is:
{user-start-code}
{generated-code}
{user-finish-code}
where "user-*-code" is an arbitrary user supplied string.
If the user start/finish code is just a function call, then it is all fine.
But if the user code is "if something:{CR}{TAB}aaaa", then the user intent simply can't be made clear from the Python's syntax: (A) is it "if (something) { aaaa }" or (B) it is "if (something) { aaaa" (and the closing "bracket" is in the finish code).
I've spent few hours tinkering, but there is simply no way to tell A from B definitively in the Python's syntax.
P.S. It is a sort of preprocessor, which allows to mix plain text with an arbitrary language. In generated output, the plain text is simply replaced with the print statements.