Mitch's Technical Blog

Flow

June 30, 2018
experiments, notes, python
permalink

I love to play with simple ideas to see where they lead. Recently, I've been experimenting with a file format and parsing algorithm for having a request "flow" from one approver to the next.

Here are the contents of an example file.

CEO
-CFO
--COO
---VP1
----Director1
-----Manager1-1
-----Manager1-2
----Director2
-----Manager2-1
-----Manager2-2
---VP2
----Director3
-----Manager3-1
-----Manager3-2
----Director4
-----Manager4-1
-----Manager4-2

And here is some python output.

    >>> from orgmodel import OrgModel
    >>> om = OrgModel('orgmodel.txt')
    >>> om.get_path('Manager4-2')
    ['Manager4-2', 'Director4', 'VP2', 'COO', 'CFO', 'CEO']

The approval chain is strictly linear: work upward from the starting node and fetch precisely one approver from any given level of indentation. The only exception is the zero level (no indentation), where all nodes are included.

Side note on "indentation": I originally was using tabs but switched to the uglier hyphen to keep things lean and simple. For starters, I don't want to deal with handling tab characters within an eventual input field on a browser form. Anyway, the actual character used is configurable, but a whitespace character will no longer work properly with the parser (because of preprocessing strip operations).

Here is a slightly different looking version of the file. Notice that CEO, CFO and COO are all now on the zero level.

CEO
CFO
COO
-VP1
--Director1
---Manager1-1
---Manager1-2
--Director2
---Manager2-1
---Manager2-2
-VP2
--Director3
---Manager3-1
---Manager3-2
--Director4
---Manager4-1
---Manager4-2

This variation of the file is functionally identical to the first. Why? The parser cares about sequence, not hierarchical relationships. They are almost the same, but not quite. I don't know why, but that fascinates me, even if it is an insignificant detail. Does that sort of relationship have a name?

Anyhow, here is the code. It runs in both python 2 and 3.

class OrgModel:
    def __init__(self, model_file, indent_char='-'):
        self.node_names = []
        self.levels = []
        self.model_file = model_file
        with open(model_file) as f:
            for line in f:
                next_name_indented = line.strip()
                next_name = next_name_indented.lstrip(indent_char)
                depth = len(next_name_indented) - len(next_name) 
                # append if line is non-blank 
                if next_name:
                    self.levels.append(depth)
                    self.node_names.append(next_name)

    def validate(self):
        levels = self.levels

        # first row must have level 0
        if levels[0] != 0:
            return False 

        for i in range(1, len(levels)):

            # if outdent, must be to a precedent level > 0
            if levels[i] < levels [i - 1]:
                if levels[i] == 0:
                    return False
                if levels[i] not in [levels[i] for i in range(0, i)]:
                    return False

            # if indent, must be only one level
            elif levels[i] - levels[i - 1] > 1:
                return False

        return True

    def get_path(self, node_name):

        # start with the node itself
        node_index = self.node_names.index(node_name)
        out = []
        out.append(self.node_names[node_index])

        # then walk upward toward the top of the model
        # (the "top" being defined as the first line in the model file)
        # the key rule is to take exactly one node from each sub-zero level,
        # plus all 0-levels "above" the "last" sub-zero level
        current_level = self.levels[node_index]
        for i in reversed(range(node_index)):
            level = self.levels[i]
            if level < current_level or level == 0:
                current_level = level
                out.append(self.node_names[i])

        return out

def main():
    """
    >>> om = OrgModel('orgmodel.txt')
    >>> om.get_path('Manager4-2')
    ['Manager4-2', 'Director4', 'VP2', 'COO', 'CFO', 'CEO']
    """

if __name__ == '__main__':
    from doctest import testmod
    testmod()

I aim to keep playing with this idea to see how far I can push it. To be continued.


Contact: hello at escapefromsql.net