30 Days of Go: Pointers and Structs

Learning Update

This post covers days 4, 5, and six of my 30 days of Golang journey. I was not able to cover a whole lot because sometimes life gets in the way. What I was able to cover was quite important though. It was the last step I needed before trying to create an application.

These are my notes on Go’s pointers and Structs.

Pointers

Go pointers allow you to pass references to values within a program. An example of this would be as follows.

package main

import "fmt"

// This is a regular function in Go
// It has a regular integer argument
func nonPointerFunc(val int) {
    val = 0
}

// This function takes a pointer as an argument
// This is denoted by the *int, this means pval is a pointer to an integer value
func pointerFunc(pval *int) {
    // The * infront of pval dereferences the pointer so that we can work with the
    // value that the pointer references
    *pval = 0
}

func main() {
    // Initialize a value
    x := 1
    // Pass the value to nonPointerFunc
    nonPointerFunc(x)
    // After execution print the value of x and you will find it is 1
    fmt.Println("Value after non pointer function: ", x)
    
    // Pass a pointer to x into pointerFunc
    // To get a pointer to a function use the syntax &value
    // &x is the pointer for x
    pointerFunc(&x)
    // Print x after the execution and you will get a value of 0
    fmt.Println("Value after pointer function:", x)
    
    // To see the pointer itself you can print it out this way.
    fmt.Println("Pointer ", &x)
}

To understand pointers better I watched this very helpful video by Junmin Lee. She explains Golang’s stack and heap to illustrate how pointers work. These are the takeaways I got based on my understanding:

  • When you pass an argument into a Go function e.g calling a function from the main function. Go will create a copy of it and that copy is what will be used for computations within the called function. The variable that was passed in from main will remain unchanged.
  • Passing a pointer of a value into a function will not create a copy of the value. The value referenced by the pointer is what will be modified by the function.
  • Using pointers is more efficient because it does not create copies of variables. By using pointers we are giving up immutability for efficiency. To understand this better I will do more research and write a blog post on this.

Structs

Structs are typed collections of fields. Go uses structs to group data together to form records.

Creating a struct

type macbook struct {
    model string
    price int
}

// creating a variable with the struct
air = macbook{"M1 air 8gb", 1000}
pro = macbook{model: "M1 pro 32gb", price:2000}
// Fields that have been ommitted will be zero-valued
proMax = macbook{price: 3500}

// struct fields can be accessed with a dot
fmt.Println(pro.model)

// dots can also be used with a struct pointer, the pointer will be automatically
// dereferenced
// Note that structs are mutable so updating the pointer will also update the initial value

sp := &air
fmt.Println(sp.price)

To write more idiomatic Go constructor functions are used to create new structs.

func newMacbook(model string, price int) *macbook {
    m := macbook{model: model, price: price}
    m.price = 1000
    return &m
}
  • Returning the pointer of a local variable is allowed because it will survive the scope of the function. To use this constructor function we write this line of code.
  • I need to read more on stack and heap allocation with this type of constructor function.
mini = newMacbook("M1 mini 8gb", 700)
fmt.Println(mini)

Methods

Go allows for methods on struct types. These methods are defined as follows:

type rect struct {
    width, height int
}

// golang methods can have pointer recivers
func (r *rect) area() int {
    return r.width * r.height
}

// golang methods can have value recivers
func (r rect) perim() int {
    return 2*r.width + 2*r.height
}

func main() {
    r := rect{width: 20, height: 10}
    
    // Go automatically handles conversion between values and pointers for method calls.
    rp := &r
    fmt.Prinln("area: ", rp.area())
}

Pointer receiver types can be used to avoid copying on method calls. They also allow the method to mutate the receiving struct.

What next

I have gone through quite a few basics and now would like to learn by actually building something. The first thing I’m going to do is exercise one from the gophercises website. The exercise is to build a quiz game. The game reads questions and answers from a CSV and allows a user to answer them from the terminal.

I’ll be documenting building this over the next few days.

April 24, 2022

30 Days of Go: Day 2 and 3

How it’s going

This is my log update for my golang learnings for days two and three. I’ve been itching to actually start building something. I’m getting pretty close to that point. But before that I’d like to go through functions, methods and structs. Once that’s done I’ll start going a little in depth with building a few simple toy applications.

Day 2

I got quite a bit done on this day.

Data structures

Arrays

  • Arrays in Go work like they do in a language like Java. All arrays need a type and size when they are declared.
  • All values in an initialized array are zero-valued
var array [40]int

// Declare and initialize
array2 := [5]int{1, 3, 4, 5, 6}

// Access an index in the array
fmt.Println(array[20])

// add something to an index in the array
array[10] = 300

// get the size of the array
fmt.Println(len(array))
  • Golang has 2D arrays. These are composed in the following way
var twoD [4][9]int
for i := 0; i < 4; i++ {
    for j := 0; j < 9; j++ {
 twoD[i][j] = i + 
    }
}

Slices

  • A slice is a data structure that works like an array with a variable size. It is like an arraylist in Java or a list in python (with the caveat that all items in the slice have to be of the same type).
  • To initialize a slice Go has a builtin function make
// Create a slice pass the type and initial size to make
sl := make([]string, 3)

// Declaring and initializing a slice
t := []string{"ab", "bc", "cd"}

// Access an item in a slice
fmt.Println(sl[2])

// set an index in a slice
sl[1] = "Hello"
  • To add more items in a slice use the builtin function append. The append function takes a slice and a value that you would like to add to that slice and returns a new slice
sl = append(sl, "new")
  • Slices can be copied using the builtin copy function
// Create a new slice with the length of the slice you want to copy
c := make([]string, len(sl))
// copies sl into c
copy(c, sl)
  • slices support slicing, like python lists
// creates a nes slice from index 1 to 2
newSlice := sl[1:3]
// creates slice from index 0 to 3 (excluding 4)
newSlice = sl[:4]
// slice from index 2 to the last one
newSlice = sl[2:]
  • slices support multidimensional data structures. The length of the inner slice can be variable.
twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
    innerLen := i + 1
    twoD[i] = make([]int, innerLen)
    for j := 0; j < innerLen; j++ {
 twoD[i][j] = i + j
    }
}

// result = [[0] [1 2] [2 3 4]]

Maps

  • These are key/value associative data structures like python dicts
  • Use make to create an empty map
m := make(map[string]int)

// declare and initialize a map
m2 := map[string]int{"foo": 4, "bar": 40}

// set key/value pairs
m["one"] = 2
m["two"] = 3

// get a value with a key
v := m["one"]

// get number of key/value pairs
fmt.Println(len(m))

// To delete a key value pair use builtin function
delete(m, "two")
  • A second value is returned when getting from a map, value, prs := map[key]. prs will be true if the key is in the map and false if it isn’t
_, prs := m["two"]
  • Blank identifier _ is used when a value is not needed. This is necessary because Go requires that you use every variable that has been declared.

Range

  • In Go we can use a range to iterate over data structures like arrays, slices, maps and strings
// array, slices
for index, value := range array {
    fmt.Println(value)
}

//map 
for key, value := range exMap {
    fmt.Printf("%s -> %s\n", key, value)
}

// iterate over map keys only
for key := range exMap {
    fmt.Println(key)
}

// iterate over strings
for index, ru := range "Golang" {
    fmt.Println(ru)
}

Day 3

I didn’t get through as much today but still interesting things were coverd.

Functions

  • Functions can have multiple return values. These functions are usually used to return the result and error values from a function
  • Variadic functions can be called with any number of arguments
  • Functions are defined in the following way:
// function structure
// function name(arguments) return type {}
func sayHello(name string) string {
    return "Hello " + name 
}
// call the function sayHello("Kacha")

// function with multiple return values
func names() (string, string) {
    return "Scott", "Ramona"
}

// variadic function
func add(nums ...int) {
    total := 0
    for _, num := range nums {
 total += num
    }
    return total
}
  • Go also supports anonymous functions which can form closures. I do not quite understand closures quite yet so I’ll have that in a different blog post.

I want to try and blog on each day but that might not be possible so I will blog my progress at least every two or three days.

April 20, 2022

30 Days of Go: Day 1

Introduction

I have decided to take part in a challenge to learn Golang in the next 30 days. I found a Go hackathon and thought this is the perfect opportunity because I have wanted to learn it for a while now. I have decided to split this learning up in a couple of ways. The idea is to build an application for the hackathon. My current app idea is a citizen science API. This API will allow users to upload photos of animals such as birds or insects.

I have split my learnings into the following categories:

  • Weeks 1 & 2: Learn the syntax and basics of Golang and do the gophercises
  • Week 3 & 4: Build out the API

Golang syntax

This information is referenced Golang documentation and especially Go by example.

Variables

  • Golang’s variables are explicitly declared like C#, Java
  • Golang allows multiple variables to be declared in one line
  • If the type has not been specified go will infer the types from initialized values
var a = "Hello world"
var age int = 8
// When the type has been declared it is used for both variables
var name, game string = "Scott Pilgrim", "vs The world"
// This is allowed because go will infer the types from initialization
var name, age = "Knives Chau", 18
  • Variables that are declared but not initialized will be zero-valued
  • All declared but not initialized variables must have a type
var one int
// one = 0
var decision bool
// decision = false
var word string
// word = ''
  • := is shorthand for declaring and initializing a variable e.g
num := 50

Constants

  • Constants in Go work the same as in other languages. A const can be a string, character, bool, or numerical type.
const test string = "Steven Stills"
  • Numerical constants perform arithmetic with arbitrary precision
  • A numerical constant has no type until it is given one from a function or is cast to a type

Flow Control

Loops

  • Go only has for loops and they can be initialized in the following ways:
i := 0
for i <= 4 {
    fmt.Println(i)
    i = i + 1
}

for j := 4; j <=8; j++ {
    fmt.Println(j)
}
  • infinite loops have no conditions and can be stopped with a break statement
for {
    fmt.Println("The infinite sadness")
    break
}
  • the continue keyword can be used to go to the next iteration of a loop
for i :=3; i <= 33; i++ {
    if i % 3 == 0 {
 continue
    }
    fmt.Println("Love evans")
}

If else

  • If else blocks are like most other programming languages
  • Parenthesis are not needed in the if statement but braces are
if x == 3 {
    fmt.Println("Young Neil")
} else if x > 5 {
    fmt.Println("Neil")
} else {
    fmt.Println("Meh")
}
  • Go’s if block can have a statement before the condition. Any variable declared in the statement can be used in all the branches of the if block.
if x := 4; x < 0 {
    fmt.Println(x)
} else if x > 1 {
    fmt.Println("True")
} else {
    fmt.Println("Do something else")
}
  • Go has no ternary if statements like javascript x == 2? "Yes": "No"

Switch statements

  • A switch statement in Go is like other programming languages
  • The default case is optional
  • Cases can have multiple expressions
  • a switch can have no expressions which will make it function like an if statement
switch i {
case 1:
    fmt.Println("One")
case 2:
    fmt.Println("Two")
case 3, 4, 5:
    fmt.Println("Others")
default:
    fmt.Println("Default is optional")
}

// Swtich with no condition
t := 5
switch {
case t < 4:
    fmt.Println("Less than 4")
default:
    fmt.Println("Else")
}
  • A type switch compares types instead of values
  • Use this switch to discover an interface’s type
  • Using .(type) does not work outside a type switch
myType := func(i interface{}){
    switch t := i.(type){
    case bool:
 fmt.Println("Im boolean")
    case int:
 fmt.Println("Im integer")
    default:
 fmt.Println("Im anything else")
    }
}

This was a fruitful first day and there’s more to come.

April 18, 2022

Creating a todo web app in < 112 lines of code with Hasura and Python

Introduction

I created this application for the Hashnode Hasura hackathon. I had some trouble thinking of an application to create. I got over my indecision by reading this post. A todo app is a valid application so that is what I made.

Built with

  • Hasura
  • Python
  • Streamlit

Setting up the backend

The backend of the application is developed with Hasura and Postgres. Doing this is really easy by following Hasura’s official documentation at this link here. It is really easy to follow along but let’s summarize the steps we can take to get our app up and running.

Hasura setup

  • Create a Hasura account and create a new project

  • Once the project has been created launch the project console. On the console click on the option to create a Heroku database.

  • Create the Users table in the launched Hasura console here

  • Create the Todos table in the Hasura console here

  • Create the foreign key relationships between the user and the todos here

Connecting our app to Hasura

Once we have our Hasura backend set up we can connect our Streamlit app to it. To do so we will need a GraphQL library to allow us to make GraphQL requests from python. Install the following libraries.

  • gql: Allows us to create GraphQL queries and mutations and requests from an API

  • aiohttp: This allows us to create asynchronous methods

  • dotenv: Allow us to read env files

pip install gql 
pip install aiohttp

Once installed create a main.py file and add the following code:

import streamlit as st
import os
from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport
from dotenv import load_dotenv

load_dotenv()


headers = {'x-hasura-admin-secret': os.environ["HASURA_ADMIN_SECRET"] }
transport = AIOHTTPTransport(url="https://curious-sailfish-33.hasura.app/v1/graphql", headers=headers)
client = Client(transport=transport, fetch_schema_from_transport=True)

First import all the required modules from the libraries we have installed.

Create headers that will be used in all the requests we make. Use a Hasura admin secret that you can get from your Hasura dashboard.

We will use an async transport protocol in our client to interact with our GraphQL endpoint.

CRUD operations

In our main.py we will create four methods to interact with our todos api.

Read Todos

def get_todos():
    query = gql(
        """
        query getTodos {
            todos {
                id
                title
                is_completed
            }
        }
        """
    )
    result = client.execute(query)
    return result["todos"]

# Save all todos into our streamlit session for use in the application
if 'todos' not in st.session_state:
    st.session_state['todos'] = get_todos()

Create a query to get all todos from our database. Save them to our local state to be used in the Streamlit application.

Create Todos

def create_todo(todo):
    query = gql(
        """
        mutation createTodo($title: String!, $user_id: String!) {
            insert_todos_one(object: {title: $title, user_id: $user_id }){
                id
                title
                is_completed
            }
        }
        """
    )
    params = {"title": todo, "user_id": "1"}

    result = client.execute(query, variable_values=params)
    todo = result['insert_todos_one']
    st.session_state.todos.append(todo)
    return result

Create a todo using a todo mutation and save the created todo into our session variable.

Update Todos

def update_todo(todo):
    query = gql(
        """
        mutation updateTodo($id: Int!, $is_completed: Boolean!){
            update_todos(where: {id: {_eq: $id}}, _set: {is_completed: $is_completed}){
                affected_rows
            }
        }
        """
    )
    params = {"id": todo["id"], "is_completed": not todo["is_completed"]}

    result = client.execute(query, variable_values=params)
    return result

Update a todo using an update mutation.

Delete Todos

def delete_todo(todo):
    query = gql(
        """
        mutation deleteTodo($id: Int!){
            delete_todos(where: {id: {_eq: $id}}){
                affected_rows
            }
        }
        """
    )
    params = {"id": todo["id"]}
    result = client.execute(query, variable_values=params)
    st.session_state.todos.remove(todo)
    return result

Finally a method to delete a todo and delete it from our local state as well.

Setting up the frontend

To set up the web application frontend I used the streamlit library . This library turns python scripts into a web application.

Let’s set up the web front end:

st.title("My todos")
with st.form(key="my_form", clear_on_submit=True):
    new_todo = st.text_input("Create a todo", value="")
    submit = st.form_submit_button("Create")
    if submit:
        create_todo(new_todo)

todos = st.session_state.todos
not_done = [todo for todo in todos if not todo["is_completed"]]
completed = [todo for todo in todos if todo["is_completed"]]
st.subheader("Not Completed")
for todo in not_done:
    col1, col2 = st.columns([3, 1])
    col1.checkbox(todo["title"], key=todo["id"])
    col2.button("delete", key=todo["id"], on_click=delete_todo, args=(todo, ))
st.subheader("Completed")
for todo in completed:
    st.checkbox(todo["title"], key=todo["id"], value=todo["is_completed"], on_change=update_todo, kwargs={"todo": todo})

Let us go through these lines of code:

st.title("My todos")

This creates a heading on the web page. We then create a form that will allow a user to input a new todo. To do this we use Streamlit’s form component.

# Clear on submit clears the input once a user 
with st.form(key="my_form", clear_on_submit=True)
    new_todo = st.text_input("Create a todo", value="")
    submit = st.form_submit_button("Create")
    # when a user submits a todo we call the function create todo
    if submit:
        create_todo(new_todo)

Streamlit allows for state management between each run of the web script. We will save all the todos that we call from our Hasura backend.

todos = st.session_state.todos
# split the todos into done and incomplete
not_done = [todo for todo in todos if not todo["is_completed"]]
completed = [todo for todo in todos if todo["is_completed"]]

The final part to the UI is displaying the todos.

st.subheader("Not Completed")
for todo in not_done:
    # Create a grid with streamlit
    col1, col2 = st.columns([3, 1])
    col1.checkbox(todo["title"], key=todo["id"])
    # A delete button calls the delete_todo method when clicked
    col2.button("delete", key=todo["id"], on_click=delete_todo, args=(todo, ))
st.subheader("Completed")
for todo in completed:
    st.checkbox(todo["title"], key=todo["id"], value=todo["is_completed"], on_change=update_todo, kwargs={"todo": todo})

Putting it all together

Adding all the code snippets for the project:

import streamlit as st
import os
from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport
from dotenv import load_dotenv

load_dotenv()


def get_gql_client(extra_headers=None):
    headers = {'x-hasura-admin-secret': os.environ["HASURA_ADMIN_SECRET"] }
    if extra_headers:
        headers += extra_headers
    transport = AIOHTTPTransport(url="https://curious-sailfish-33.hasura.app/v1/graphql", headers=headers)
    client = Client(transport=transport, fetch_schema_from_transport=True)
    return client

headers = {'x-hasura-admin-secret': os.environ["HASURA_ADMIN_SECRET"] }
transport = AIOHTTPTransport(url="https://curious-sailfish-33.hasura.app/v1/graphql", headers=headers)
client = Client(transport=transport, fetch_schema_from_transport=True)


def get_todos():
    query = gql(
        """
        query getTodos {
            todos {
                id
                title
                is_completed
            }
        }
        """
    )
    result = client.execute(query)
    return result["todos"]


if 'todos' not in st.session_state:
    st.session_state['todos'] = get_todos()


def create_todo(todo):
    query = gql(
        """
        mutation createTodo($title: String!, $user_id: String!) {
            insert_todos_one(object: {title: $title, user_id: $user_id }){
                id
                title
                is_completed
            }
        }
        """
    )
    params = {"title": todo, "user_id": "1"}

    result = client.execute(query, variable_values=params)
    todo = result['insert_todos_one']
    st.session_state.todos.append(todo)
    return result


def update_todo(todo):
    query = gql(
        """
        mutation updateTodo($id: Int!, $is_completed: Boolean!){
            update_todos(where: {id: {_eq: $id}}, _set: {is_completed: $is_completed}){
                affected_rows
            }
        }
        """
    )
    params = {"id": todo["id"], "is_completed": not todo["is_completed"]}

    result = client.execute(query, variable_values=params)
    return result


def delete_todo(todo):
    query = gql(
        """
        mutation deleteTodo($id: Int!){
            delete_todos(where: {id: {_eq: $id}}){
                affected_rows
            }
        }
        """
    )
    params = {"id": todo["id"]}
    result = client.execute(query, variable_values=params)
    st.session_state.todos.remove(todo)
    return result


st.title("My todos")
with st.form(key='my_form', clear_on_submit=True):
    new_todo = st.text_input("Create a todo", value="", key="d")
    submit = st.form_submit_button("Create")
    if submit:
        create_todo(new_todo)

todos = st.session_state.todos
not_done = [todo for todo in todos if not todo["is_completed"]]
completed = [todo for todo in todos if todo["is_completed"]]
st.subheader("Not Completed")
for todo in not_done:
    col1, col2 = st.columns([3, 1])
    col1.checkbox(todo["title"],key=todo["id"], value=todo["is_completed"], on_change=update_todo, args=todo)
    col2.button("Delete", key=todo["id"], on_click=delete_todo, args=(todo, ))
st.subheader("Completed")
for todo in completed:
    st.checkbox(todo["title"],key=todo["id"], value=todo["is_completed"], on_change=update_todo, kwargs={"todo": todo})

Deploy the application

To deploy our application create a requirements.txt file in our folder and add the following:

aiohttp
streamlit
python-dotenv
gql

We then push this repo to Github.

Sign up for an account on Streamlit and link it to your GitHub account. Create a new application and select the repository that was pushed. Streamlit will allow you to pick the branch and the file that the application will run from.

Create streamlit app

Click on Advanced settings to add our environment variables. This is where we will add the Hasura admin secret.

Streamlit env variables settings

Once saved hit deploy and viola your application is ready to be used and shared.

What I learned

I learned quite a bit during this hackathon, it was my first time using Hasura. The following are a few things that I learned:

  • Creating a Hasura GraphQL backend

  • Connecting a Hasura API to python

  • Local state in a python Streamlit application

  • Auth0 for authentication purposes

Things I struggled with

The main thing I struggled with was getting Hasura’s authentication with Auth0 to play well with Streamlit. This was more a problem with me trying to fit a triangle peg in a square hole. Streamlit is not a conventional web framework so the concept of a redirect URL does not work with it. I tried to create an API backend authentication using a flask JWT API tutorial here.

I was able to get the sign-up route working fine but logging in was an issue because each JWT I generated was invalid. I’m sure there is something I missed in the documentation but for the life of me, I could not get it to work. I will revisit this and also push the code to my Github page.

Try it out

Find the relevant links below:

March 29, 2022 hackathon

Creating a Whatsapp Chat analyser web app with Symbl.ai and python in less than 100 lines of code

This application will take an .txt export of a Whatsapp chat. The application will then run conversational analysis on the .txt file. Symbl.ai will then extract information like follow ups, topics and action items.

The application requires the following libraries:

  • streamlit: This library converts python scripts into web applications.
  • symbl.ai: This is a conversational A.I library that can get insights from audio and text.

Starting the Streamlit app

Streamlit is a great library I found that can convert python scripts into a web application.

Lets create an application that will accept a whatsapp txt chat file and parse the chat data.

import streamlit as st


chat_file = st.file_uploader("Select chat file")

if chat_file:
    conversation_request = handle_uploaded_file(chat_file)

To handle the file and extract the messages in a format that symbl will understand we need to create a function to do it.

def handle_uploaded_file(file):
    chat = file.read().decode("utf-8")

    # Split each line from the file into a list and strip all whitespace
    lines = chat.splitlines()
    lines = [line.strip() for line in lines]

    messages = []
    cleaned_lines = []

    # If line does not start with a Whatsapp date format [m/d/y, h:m:s am] then append it to the previous line.
    for x in range(len(lines)):
        if lines[x].find("[") == -1:
            cleaned_lines[-1] = f"{cleaned_lines[-1]} {lines[x]}"
            continue
        cleaned_lines.append(lines[x])

    # pares the time, name and message from a line
    for line in cleaned_lines:
        st_dt, en_dt = line.find("["), line.find("]")
        time = line[st_dt:en_dt].replace("[", "")
        name = line[en_dt:].split(":")[0].replace("]", "")
        st_m = line[en_dt:].find(":")
        message = line[en_dt:][st_m:].replace(":", "")

        # Create messages in the format Symbl requires
        messages.append(
            {
            "duration": {"startTime": arrow.get(time, 'M/D/YY, H:mm:ss A').format(), "endTime": arrow.get(time, 'M/D/YY, H:mm:ss A').format()},
            "payload": {"content": message, "contentType": "text/plain"},
            "from": {"name": name, "userId": name},
            }
        )

    return {
        "name": "Chat",
            "confidenceThreshold": 0.6,
            "detectPhrases": True,
            "messages": messages,
    }

Once we have the messages from the handle request file we can analyse it with Symbl.ai

import symbl

...

conversation_object = symbl.Text.process(payload=conversation_request)

Displaying the results

Add some display components using streamlit.

with st.expander("Topics"):
    with st.spinner("Getting topics"):
        topics = symbl.Conversations.get_topics(conversation_id=conversation_id)
    for topic in topics.topics:
        st.write(f"- {topic.text}")

with st.expander("Action Points"):
    with st.spinner("Getting action items"):
        action_items = symbl.Conversations.get_action_items(conversation_id=conversation_id)
    for item in action_items.action_items:
        st.write(f"- {item.text}")

with st.expander("Questions"):
    questions = symbl.Conversations.get_questions(conversation_id=conversation_id)
    for question in questions.questions:
        st.write(f"- {question.text}")

with st.expander("Follow ups"):
    follow_ups = symbl.Conversations.get_follow_ups(conversation_id=conversation_id)
    for follow_up in follow_ups.follow_ups:
        st.write(f"- {follow_up.text}")

The final code file

import streamlit as st
import arrow
import symbl


def handle_uploaded_file(file):
    chat = file.read().decode("utf-8")
    lines = chat.splitlines()
    lines = [l.strip() for l in lines]
    messages = []
    cleaned_lines = []
    for x in range(len(lines)):
        if lines[x].find("[") == -1:
            cleaned_lines[-1] = f"{cleaned_lines[-1]} {lines[x]}"
            continue
    cleaned_lines.append(lines[x])
    for line in cleaned_lines:
        st_dt, en_dt = line.find("["), line.find("]")
        time = line[st_dt:en_dt].replace("[", "")
        name = line[en_dt:].split(":")[0].replace("]", "")
        st_m = line[en_dt:].find(":")
        message = line[en_dt:][st_m:].replace(":", "")
        messages.append(
            {
                "duration": {"startTime": arrow.get(time, 'M/D/YY, H:mm:ss A').format(), "endTime": arrow.get(time, 'M/D/YY, H:mm:ss A').format()},
                "payload": {"content": message, "contentType": "text/plain"},
                "from": {"name": name, "userId": name},
            }
        )
    return {
        "name": "Chat",
            "confidenceThreshold": 0.6,
            "detectPhrases": True,
            "messages": messages,
    }


chat_file = st.file_uploader("Select chat file")

if chat_file:
    conversation_request = handle_uploaded_file(chat_file)
    with st.spinner("Analysing Text"):
        conversation_object = symbl.Text.process(payload=conversation_request)
    conversation_id = conversation_object.get_conversation_id()
    st.write(conversation_id)

    with st.expander("Topics"):
        with st.spinner("Getting topics"):
            topics = symbl.Conversations.get_topics(conversation_id=conversation_id)
        for topic in topics.topics:
            st.write(f"- {topic.text}")

    with st.expander("Action Points"):
        with st.spinner("Getting action items"):
            action_items = symbl.Conversations.get_action_items(conversation_id=conversation_id)
        for item in action_items.action_items:
            st.write(f"- {item.text}")

    with st.expander("Questions"):
        questions = symbl.Conversations.get_questions(conversation_id=conversation_id)
        for question in questions.questions:
            st.write(f"- {question.text}")

    with st.expander("Follow ups"):
        follow_ups = symbl.Conversations.get_follow_ups(conversation_id=conversation_id)
        for follow_up in follow_ups.follow_ups:
            st.write(f"- {follow_up.text}")

Screenshots of our final application

What Chat image

Whatchat search results

Whatchat analysis results

March 24, 2022 Python A.I streamlit

What I learned in February

In my quest to catalog my developer journey here are a few things I have learned this month.

React/Javascript

Local development of React applications with an express Api can have CORS errors. To solve this React has an option to serve the frontend on the same port as the backend. This is the proxy option that can be set in the React application’s package.json file.

To do this open your react package.json file and add the following option:

    "proxy": "http://localhost:<EXPRESS PORT>"

To read more on this read the React documentation here.

Python

Running a python module or a script has a subtle difference: -m. To run a python module add the -m” tag.

So if you have a module like this:

hello/
    __init__.py
    __main__.py
    somescript.py

You can run it like this:

python -m <module-name>
python -m hello

As a note the module-name needs to be the name of the module not a string.

Django

Using Sass with Django.

Install the following libraries

pip install lib sass django-compressor django-sass-processor

Add the Django sass processor to your installed apps in your Django settings. Whilst in the settings add the following static files settings.

INSTALLED_APPS = [
    ...,
    'sass_processor',
    ...
]

STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    'sass_processor.finders.CssFinder',
]

SASS_PROCESSOR_ROOT = os.path.join(BASE_DIR, 'static')

Pick your UI framework e.g. UIKit, Bootstrap, or Materialize. Copy the sass folder into your static files folder.

Create a main.scss file in the static files folder. This file is the primary way that the sass processor will interact with the sass framework.

In the base template add the following sass tags:

<link href="{% sass_src 'main.scss' %}" rel="stylesheet" type="text/css">

Django Deployment

Deploying an app to the Digital Ocean app platform with static files on digital ocean spaces. This will be a separate blog post.

March 8, 2022 Python Django Javascript React