Where to Discuss?

Local Group

Preface

Goal: Dynamically showing live data in terminal user interface using Python Rich Library from textualize.

This is the data model. We are going to wrapped the example data in a class.

Requirement

Fake Data

For demo purpose, we need these stuff:

  1. Displayed in table, with a few additional text message
  2. Number of row changed, dynamically.
  3. Number of row can’t be too few.
  4. Value changed, from time to time.

Example Case

I’m going to use months data as row header. From January to December, but shown dynamically. Only month with data will be shown, other will be hidden.

For each month I have these columns:

  1. Budget
  2. Actual
  3. Gap Difference

3: Data Modelling: Simple

Month Only

Python Source Code

A class along with instantiation, bundled in one file.

Class: Skeleton

Since we are going to utilize this class from outside, We need to care a bit about method visibility.

Here is in this class.

  • One MONTHS constant property,
  • one constructor
  • Two private method
  • One public method (main)
class MonthData:
  MONTHS = {...}

  def __init__(self):
  def __get_month_rnd(self):
  def __get_months_dict(self):
  def main(self):

The months data is simply a dictionary:

  MONTHS = {
    1: 'January',   2: 'February',
    3: 'March',     4: 'April',
    5: 'May',       6: 'June',
    7: 'July',      8: 'August',
    9: 'September', 10: 'October',
    11: 'November', 12: 'December'}

And we will call the main method as below:

month_data = MonthData()
month_data.main()

Python: Data Model: Simple

Constructor

This is the method actually called, when we create class instance.

We have two main properties:

  • start month
  • end month

For example 5, 9, means may to september

  def __init__(self):
    # save initial parameter
    [self.start, self.end] = self.__get_month_rnd()

This will call private method __get_month_rnd().

Private Method

For internal use only

Private method only available in class only. And can’t be called from outside class.

Here we can set start and end, using random number.

We are going to constraint number of row, not to be too few. At least 4 month rows. We measure this by using end - start > 3:.

  def __get_month_rnd(self):
    while True:
      start = randint(1, 12)
      end   = randint(start, 12)
      if end - start > 3: break
    return [start, end]

And also we can get the month name.

  def __get_months_dict(self):
    m_all   = list(self.MONTHS.items())
    m_slice = m_all[self.start:self.end]
    return dict(m_slice)

Public Method

Public method can be accessed from outside class.

  def main(self):
    print(self.__get_months_dict())

Such as

month_data = MonthData()
month_data.main()

Python: Data Model: Simple

Example Output in CLI

The result of this function is similar as below:

❯ python month_13.py
{4: 'April', 5: 'May', 6: 'June', 7: 'July'}

Python: Data Model: Simple


4: Data Modelling: Record Detail

With Budgeting

We are going to add these record structure:

  • Budget
  • Actual
  • Gap Difference

Python Source Code

A class along with running part (instantiation), bundled in one file.

Class: Skeleton

class MonthData:
  MONTHS = {...}
  def __init__(self, baseline):
  def __get_indices(self):
  def __get_month_rnd(self):
  def __get_months_dict(self):
  def __init_random(self):
  def __init_fakes(self):
  def main(self):

Python: Data Model: Record Detail

Main Script

We start with import.

from random import randint
from rich import print

And we will call the main method as below:

month_data = MonthData(1500)
month_data.main()

Note that we have one argument. Our budget base.

Constructor

We change a bit.

  def __init__(self, baseline):
    # save initial parameter
    self.baseline = baseline

    # build initial data
    [self.start, self.end] = self.__get_month_rnd()
    self.__init_fakes()

  def __get_indices(self):
    return list(range(self.start, self.end+1))

Helper Method

Since we are going to refer to index many time, this small helper would help

  def __get_indices(self):
    return list(range(self.start, self.end+1))

There is nothing new with these two method.

  def __get_month_rnd(self):
  def __get_months_dict(self):

Python: Data Model: Record Detail

Method: Faking Data

Simulate budget randomly

Just these two functions.

We iterate for each indices, and get the random data using __init_random()

  def __init_fakes(self):
    self.fakes = {}
    for key in self.__get_indices():
      self.fakes[key] = self.__init_random()

While the function in detail is as simple as below:

  • Budget: random data.
  • Actual: random data.
  • Gap Difference: based on calculation.
  def __init_random(self):
    budget = self.baseline + randint(-20, 100)
    actual = self.baseline + randint(-50, 10)
    gap    = budget - actual

    return [budget, actual, gap]

Python: Data Model: Record Detail

We put the data in self.fakes.

Class Instance

Finally we create instance, along with variable initialization, and call the main method.

class MonthData:
  ...
  def main(self):
    print(self.__get_months_dict())
    print(self.fakes)

# Program Entry Point
month_data = MonthData(1500)
month_data.main()

Example Output in CLI

Consider examine the data.

❯ python month_14.py
{
    2: 'February',
    3: 'March',
    4: 'April',
    5: 'May',
    6: 'June',
    7: 'July',
    8: 'August',
    9: 'September',
    10: 'October',
    11: 'November'
}
{
    2: [1494, 1497, -3],
    3: [1508, 1450, 58],
    4: [1531, 1470, 61],
    5: [1522, 1497, 25],
    6: [1572, 1473, 99],
    7: [1587, 1497, 90],
    8: [1548, 1459, 89],
    9: [1585, 1486, 99],
    10: [1586, 1510, 76],
    11: [1525, 1487, 38]
}

Since we are using random data, the result would be different from time to time.

Python: Data Model: Record Detail

Both dictionary output looks like a JSON data. Since I use this print method form rich, The result looks pretty.

from rich import print

In the next section we are going to reproduce data dynamically.


5: Data Modelling: Dynamic

Change based on previous data

Why dynamically reproduce fake data? Because we are going to make nice rich demo. So this could looks like data captured from somewhere.

The rich demo should show capability, of rich table to add or remove row, from time to time, continuously.

Python Source Code

Since we are going to reus the class in our main script, we should separate the class and the running part.

Class: Skeleton

We have a bunch of new methods here.

from random import randint

# Fake Data Generator Class 
class MonthData:
  MONTHS = {...}
  def __init__(self, baseline):

  def __get_indices(self):
  def __get_month_rnd(self):
  def __update_month_rnd(self):
  def __get_months_dict(self):

  def __init_random(self):
  def __init_fakes(self):

  def __update_fakes(self):
  def __update_fakes_value(self):

  def get_strings(self):
  def update(self):

Though this looks scary. Don’t be intimidated by these bunch of method.

Python: Data Model: Dynamic

Most of the methods below is the same as previous. There is nothing change for these methods.

  def __init__(self, baseline):
  def __get_indices(self):
  def __get_month_rnd(self):

  def __get_months_dict(self):
  def __init_random(self):
  def __init_fakes(self):

Method: Update Month Rows

Month row can’t be less than than three rows. If it less, start the loop.

  def __update_month_rnd(self):
    while True:
      [start, end] = [self.start, self.end]

      if randint(0, 1):
        start += randint(-1, 1)
        if start < 1: start = 1
      else:
        end += randint(-1, 1)
        if end > 12: end = 12
      if end - start > 3:
        break

    [self.start, self.end] = [start, end]

Python: Data Model: Dynamic

To make the animation looks smooth, we only add/remove one row at a time. We have to choose, whether changing the head start, or the tail end.

      if randint(0, 1):
        start += ...
      else:
        end += ...
      if end - start > 3:

To prevent infinite loop, the start and end data only be saved, if the data valid. This is why we have these notation:

  def __update_month_rnd(self):
    while True:
      [start, end] = [self.start, self.end]
      ...

    [self.start, self.end] = [start, end]

Method: Update Months

Add/Remove rows of months

After we got a new key, we must also update the data in self.fakes.

In order to do this, we should compare current head start and tail end, with obsolete self fakes.

Removing unwanted key, and repopulate new key with random data.

  def __update_fakes(self):
    # remove key
    unwanted = set(self.fakes.keys()) \
             - set(self.__get_indices())
    for key in unwanted:
      self.fakes.pop(key, None)

    # add key
    wanted = set(self.__get_indices()) \
           - set(self.fakes.keys())
    for key in wanted:
      self.fakes[key] = self.__init_random()

Method: Go further with details

We can also change the detail a bit for finishing touch.

  • Budget: remain the same.
  • Actual: add/substract with random fluctuation.
  • Gap Difference: based on calculation.
  def __update_fakes_value(self):
    for key in self.__get_indices():
      [budget, actual, gap] = self.fakes[key]

      actual = actual + randint(-10, 10)
      gap    = budget - actual

      self.fakes[key] = [budget, actual, gap]

Python: Data Model: Dynamic

Public Method

First we update the data by correct order

  def update(self):
    self.__update_month_rnd()
    self.__update_fakes()
    self.__update_fakes_value()

Then get the record as string value, to be feed to rich panel.

We have to rebuild the structure, to suit our need. Such as, convert all item to string, because rich panel only accept string value.

  def get_strings(self):
    rows = {}
    for key in self.__get_indices():
      [budget, actual, gap] = self.fakes[key]
      rows[key] = [self.MONTHS[key],
        str(budget), str(actual), str(gap)]
    return rows

Note that you can also combine these function above, into just one function with return value.

After all this is just an example.

Python: Data Model: Dynamic

Run

From separate python script

We can import our class, and use it in our script.

import month_15
from rich import print

# Program Entry Point
month_data = month_15.MonthData(1300)
print(month_data.get_strings())

for c in range(1, 5+1):
  month_data.update()
  print(month_data.get_strings())

Python: Data Model: Dynamic

Output in CLI

The result of this function is similar as below:

❯ python month_15_run.py

{
    6: ['June', '1342', '1305', '37'],
    7: ['July', '1312', '1267', '45'],
    8: ['August', '1289', '1279', '10'],
    9: ['September', '1320', '1279', '41'],
    10: ['October', '1294', '1255', '39'],
    11: ['November', '1345', '1286', '59']
}
{
    5: ['May', '1357', '1307', '50'],
    6: ['June', '1342', '1297', '45'],
    7: ['July', '1312', '1262', '50'],
    8: ['August', '1289', '1275', '14'],
    9: ['September', '1320', '1278', '42'],
    10: ['October', '1294', '1253', '41'],
    11: ['November', '1345', '1294', '51']
}

With number of month might changed from time to time continuosly.

Python: Data Model: Dynamic

We are done with data modelling.


What is Next 🤔?

Enough data example

I can’t wait to apply the data to rich Panel.

Consider continue reading [ Python - Rich - Live Asynchronous ].