Working on a Sudoku Solver in Python (Source Code)

This is a document previous to a live code review session.

It has the information to prepare for the upcoming code review session, where I plan to share the lessons learned, decision I took, mistakes I did, refactors I had to overcome, and tentatively we will refactor code in order to add some Unit Testing.

History

I used to play sudoku with my family, so from time to time I do by myself.

Once I found a sudoku that was impossible and it happened that it was a typo from the newspaper, so, when I found another impossible sudoku I wanted to know if it was me, or if there was a typo or similar, so I decided to write a Sudoku Solver that will solve the sudoku for me.

The bad guys

I had problems solving these two sudokus:

Some Screenshots

The Source Code

You can clone the project from here:

https://gitlab.com/carles.mateo/sudo-ku-solver

You will have to install colorama package, as I used it for giving colors to the output:

pip3 install colorama

The main program sudokusolver.py:

import copy
from lib.colorutils import ColorUtils


class SudokuMap():
    
    def __init__(self, i_width, i_height, o_color=ColorUtils()):
        self.i_width = i_width
        self.i_height = i_height
        self.o_color = o_color

        self.a_map = self.generate_empty_map()

    def generate_empty_map(self):
        a_map = []
        a_row = []
        a_i_possible_numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
        for i_x in range(self.i_width):
            a_row.append(a_i_possible_numbers.copy())

        for i_y in range(self.i_height):
            a_map.append(copy.deepcopy(a_row))

        return a_map

    def set_number(self, i_number, i_x, i_y):
        """
        Sets a well known (already defined in the original map) number for a position
        :param i_number:
        :param i_x:
        :param i_y:
        :return:
        """
        self.a_map[i_y][i_x] = [i_number]

    def detect_and_remove_a_number_from_possibles_from_a_row(self, i_y):
        """
        We will elinate this possibility from the row
        :return: Boolean
        """

        b_found = False
        self.o_color.print_label("Detecting numbers to remove from row " + str(i_y))

        for i_x in range(0, self.i_width):
            a_i_numbers_possible = self.a_map[i_y][i_x]
            if len(a_i_numbers_possible) == 1:
                b_found = True
                i_number_found = self.a_map[i_y][i_x][0]
                print("Found a number that will be removed from horizontal and vertical and in quadrant", i_number_found, "at", i_x, i_y)
                self.remove_a_number_from_possibles_in_a_row(i_number_to_remove=i_number_found, i_y=i_y)
                self.remove_a_number_from_possibles_in_a_column(i_number_to_remove=i_number_found, i_x=i_x)
                self.remove_a_number_from_possibles_in_quadrant(i_number_to_remove=i_number_found, i_x=i_x, i_y=i_y)

        return b_found

    def remove_a_number_from_possibles_in_a_row(self, i_number_to_remove, i_y):
        """
        Removes a number from the list of possibles in that row
        :param i_number_to_remove:
        :param i_y:
        :return:
        """

        self.o_color.print_label("> Scanning for removing " + str(i_number_to_remove) + " in row " + str(i_y))

        for i_x in range(0, self.i_width):
            a_i_numbers_possible = self.a_map[i_y][i_x]
            if len(a_i_numbers_possible) == 1 and a_i_numbers_possible[0] == i_number_to_remove:
                # This is the right cell, ignore it
                pass
            else:
                # Subtract the number from the sequence
                if i_number_to_remove in a_i_numbers_possible:
                    a_i_numbers_possible_old = a_i_numbers_possible.copy()
                    a_i_numbers_possible.remove(i_number_to_remove)
                    print("> Removed", i_number_to_remove, "From:", str(i_x) + "x" + str(i_y), a_i_numbers_possible_old, "Pending:", a_i_numbers_possible)
                    self.a_map[i_y][i_x] = a_i_numbers_possible
                    if len(a_i_numbers_possible) == 1:
                        # Trigger it again for the number recently discovered
                        i_new_number_to_remove = a_i_numbers_possible[0]
                        self.o_color.print_success("> Found " + str(i_new_number_to_remove) + " From: " + str(i_x) + "x" + str(i_y))
                        self.remove_a_number_from_possibles_in_a_row(i_number_to_remove=i_new_number_to_remove, i_y=i_y)
                        self.remove_a_number_from_possibles_in_a_column(i_number_to_remove=i_new_number_to_remove, i_x=i_x)
                        self.remove_a_number_from_possibles_in_quadrant(i_number_to_remove=i_new_number_to_remove, i_x=i_x, i_y=i_y)

        self.o_color.print_label("> Leaving scan for " + str(i_number_to_remove) + " in row " + str(i_y))

    def remove_a_number_from_possibles_in_a_column(self, i_number_to_remove, i_x):
        """
        Removes a number from the list of possibles in that row
        :param i_number_to_remove:
        :param i_y:
        :return:
        """

        self.o_color.print_label("V Scanning for removing " + str(i_number_to_remove) + " in col " + str(i_x))

        for i_y in range(0, self.i_height):
            a_i_numbers_possible = self.a_map[i_y][i_x]
            if len(a_i_numbers_possible) == 1 and a_i_numbers_possible[0] == i_number_to_remove:
                # This is the right cell, ignore it
                pass
            else:
                # Subtract the number from the sequence
                if i_number_to_remove in a_i_numbers_possible:
                    a_i_numbers_possible_old = a_i_numbers_possible.copy()
                    a_i_numbers_possible.remove(i_number_to_remove)
                    print("V Removed", i_number_to_remove, "From:", i_x, i_y, a_i_numbers_possible_old, "Pending:", a_i_numbers_possible)
                    # @TODO: Remove, as it's a pointer it is not needed
                    self.a_map[i_y][i_x] = a_i_numbers_possible
                    if len(a_i_numbers_possible) == 1:
                        # Trigger it again for the number recently discovered
                        i_new_number_to_remove = a_i_numbers_possible[0]
                        self.o_color.print_success("Found " + str(i_new_number_to_remove) + " From: " + str(i_x) + " " + str(i_y))
                        self.remove_a_number_from_possibles_in_a_row(i_number_to_remove=i_new_number_to_remove, i_y=i_y)
                        self.remove_a_number_from_possibles_in_a_column(i_number_to_remove=i_new_number_to_remove, i_x=i_x)
                        self.remove_a_number_from_possibles_in_quadrant(i_number_to_remove=i_new_number_to_remove, i_x=i_x, i_y=i_y)

        self.o_color.print_label("V Leaving scan for " + str(i_number_to_remove) + " in col " + str(i_x))

    def remove_a_number_from_possibles_in_quadrant(self, i_number_to_remove, i_x, i_y):
        """

        :param i_number_to_remove:
        :param i_x:
        :param i_y:
        :return:
        """

        i_x_quadrant = int(i_x / 3)
        i_y_quadrant = int(i_y / 3)

        i_x_ini = i_x_quadrant * 3
        i_x_end = i_x_ini + 2

        i_y_ini = i_y_quadrant * 3
        i_y_end = i_y_ini + 2

        for i_y_rel in range(i_y_ini, i_y_end + 1):
            for i_x_rel in range(i_x_ini, i_x_end + 1):
                a_i_numbers_possible = self.a_map[i_y_rel][i_x_rel]
                if len(a_i_numbers_possible) == 1 and a_i_numbers_possible[0] == i_number_to_remove:
                    # This is the right cell, ignore it
                    pass
                else:
                    # Subtract the number from the sequence
                    if i_number_to_remove in a_i_numbers_possible:
                        a_i_numbers_possible_old = a_i_numbers_possible.copy()
                        a_i_numbers_possible.remove(i_number_to_remove)
                        print("X Removed", i_number_to_remove, "From:", i_x_rel, i_y_rel, a_i_numbers_possible_old, "Pending:", a_i_numbers_possible)
                        # Nota: Here I had a bug and I was "liant-la parda"
                        # if len(a_i_numbers_possible) == 1:
                        #     # Trigger it again for the number recently discovered
                        #     i_new_number_to_remove = a_i_numbers_possible[0]
                        #     string_ints = [str(int) for int in ints]
                        #     self.o_color.print_success("X Found " + str(i_new_number_to_remove) + " From: " + str(i_x) + "x" + str(i_y) + "[]")
                        #     self.remove_a_number_from_possibles_in_a_row(i_number_to_remove=i_new_number_to_remove, i_y=i_y)
                        #     self.remove_a_number_from_possibles_in_a_column(i_number_to_remove=i_new_number_to_remove, i_x=i_x)

    def check_if_number_possibles_in_quadrant_is_unique(self, i_number_to_check, i_x, i_y):
        """

        :param i_number_to_remove:
        :param i_x:
        :param i_y:
        :return: b_found
        """

        i_x_quadrant = int(i_x / 3)
        i_y_quadrant = int(i_y / 3)

        i_x_ini = i_x_quadrant * 3
        i_x_end = i_x_ini + 2

        i_y_ini = i_y_quadrant * 3
        i_y_end = i_y_ini + 2

        i_number_of_occurrences_found = 0
        i_x_position_number = 0
        i_y_position_number = 0

        b_unique = False

        for i_y_rel in range(i_y_ini, i_y_end + 1):
            for i_x_rel in range(i_x_ini, i_x_end + 1):
                a_i_numbers_possible = self.a_map[i_y_rel][i_x_rel]
                for i_number_in_possibles in a_i_numbers_possible:
                    if len(a_i_numbers_possible) > 1 and i_number_in_possibles == i_number_to_check:
                        # This is the right cell, ignore it
                        i_number_of_occurrences_found += 1
                        i_x_position_number = i_x_rel
                        i_y_position_number = i_y_rel
                        if i_number_of_occurrences_found > 1:
                            # Unsuccessful
                            break

        if i_number_of_occurrences_found == 1:
            # Success!
            a_i_numbers_possible = [i_number_to_check]
            self.a_map[i_y_position_number][i_x_position_number] = a_i_numbers_possible
            b_unique = True

        return b_unique, i_x_position_number, i_y_position_number

    def check_if_number_possibles_in_row_is_unique(self, i_number_to_check, i_y):
        """

        :param i_number_to_check:
        :param i_x:
        :param i_y:
        :return:
        """

        i_number_of_occurrences_found = 0
        i_x_position_number = 0
        i_y_position_number = 0

        b_unique = False

        for i_x_rel in range(0, 9):
            a_i_numbers_possible = self.a_map[i_y][i_x_rel]
            for i_number_in_possibles in a_i_numbers_possible:
                if len(a_i_numbers_possible) > 1 and i_number_in_possibles == i_number_to_check:
                    # This is the right cell, ignore it
                    i_number_of_occurrences_found += 1
                    i_x_position_number = i_x_rel
                    i_y_position_number = i_y
                    if i_number_of_occurrences_found > 1:
                        # Unsuccessful
                        break

        if i_number_of_occurrences_found == 1:
            # Success!
            a_i_numbers_possible = [i_number_to_check]
            self.a_map[i_y_position_number][i_x_position_number] = a_i_numbers_possible
            b_unique = True

        return b_unique, i_x_position_number, i_y_position_number

    def get_map_drawing_as_string(self, a_map_alternative=None):
        s_map = ""
        i_counter_y = 0
        s_separator_rows = "="

        a_map_to_use = self.a_map
        if a_map_alternative is not None:
            a_map_to_use = a_map_alternative

        s_map = s_map + s_separator_rows * 37 + "\n"
        for a_row in a_map_to_use:
            i_counter_y += 1
            if i_counter_y == 3:
                i_counter_y = 0
                s_separator_rows = "="
            else:
                s_separator_rows = "-"

            s_map = s_map + "|"
            i_counter = 0
            for a_i_numbers_possible in a_row:
                i_counter += 1

                if len(a_i_numbers_possible) == 1:
                    s_number = str(a_i_numbers_possible[0])
                else:
                    s_number = " "

                if i_counter == 3:
                    s_separator = "|"
                    i_counter = 0
                else:
                    s_separator = "¦"
                s_map = s_map + " " + s_number + " " + s_separator
            s_map = s_map + "\n"

            s_map = s_map + s_separator_rows * 37 + "\n"

            # Replace 0 by " "
            s_map = s_map.replace("0", " ")

        s_map = s_map + "\n\n"
        i_total_numbers_found, a_s_numbers_found = self.get_total_numbers_found()
        s_map = s_map + "Total numbers found: " + str(i_total_numbers_found) + " Numbers found: " + " ".join(a_s_numbers_found) + "\n"

        return s_map

    def get_map_drawing_of_possibles_as_string(self, a_map_alternative=None):
        s_map = ""
        i_counter_y = 0
        s_separator_rows = "="

        a_map_to_use = self.a_map
        if a_map_alternative is not None:
            a_map_to_use = a_map_alternative

        s_map = s_map + self.o_color.color_blue(s_separator_rows * ((9 * ( 9 + 2 )) + 10)) + "\n"
        for a_row in a_map_to_use:
            i_counter_y += 1
            if i_counter_y == 3:
                i_counter_y = 0
                s_separator_rows = "="
            else:
                s_separator_rows = "-"

            s_map = s_map + self.o_color.color_blue("|")
            i_counter = 0
            for a_i_numbers_possible in a_row:
                i_counter += 1

                if len(a_i_numbers_possible) == 1:
                    # The right number
                    s_number = str(a_i_numbers_possible[0]).center(9)
                    s_number = self.o_color.color_success(s_number)
                else:
                    a_i_numbers_possible_string = []
                    for i_number in a_i_numbers_possible:
                        s_number = str(i_number)
                        # Replace by the color sequence
                        if i_number == 2:
                            s_number = self.o_color.color_red(s_number)
                        if i_number == 3:
                            s_number = self.o_color.color_yellow(s_number)
                        if i_number == 4:
                            self.o_color.color_magenta(s_number)
                        a_i_numbers_possible_string.append(s_number)
                    # s_number = "".join(a_i_numbers_possible_string).ljust(9)
                    s_number = "".join(a_i_numbers_possible_string) + " " * (9-len(a_i_numbers_possible))

                if i_counter == 3:
                    s_separator = self.o_color.color_blue("|")
                    i_counter = 0
                else:
                    s_separator = self.o_color.color_blue("¦")
                s_map = s_map + " " + s_number + " " + s_separator
            s_map = s_map + "\n"

            s_map = s_map + self.o_color.color_blue(s_separator_rows * ((9 * (9 + 2)) + 10)) + "\n"

            # Replace 0 by " "
            s_map = s_map.replace("0", " ")

        return s_map

    def get_total_numbers_found(self):

        i_total_numbers_found = 0
        a_s_numbers_found = []

        for i_y in range(0, self.i_height):
            for i_x in range(0, self.i_width):
                a_i_numbers_possible = self.a_map[i_y][i_x]
                if len(a_i_numbers_possible) == 1:
                    i_total_numbers_found = i_total_numbers_found + 1
                    i_number_found = self.a_map[i_y][i_x][0]
                    s_number_found = str(i_number_found)
                    if s_number_found not in a_s_numbers_found:
                        a_s_numbers_found.append(s_number_found)

        return i_total_numbers_found, a_s_numbers_found


if __name__ == "__main__":

    o_color = ColorUtils()

    o_map = SudokuMap(9, 9, o_color=o_color)
    o_map.set_number(i_number=1, i_x=1, i_y=0)
    o_map.set_number(3, 4, 0)
    o_map.set_number(8, 7, 0)

    o_map.set_number(8, 0, 1)
    o_map.set_number(7, 3, 1)
    o_map.set_number(4, 5, 1)
    o_map.set_number(6, 8, 1)

    o_map.set_number(3, 2, 2)
    o_map.set_number(9, 6, 2)

    o_map.set_number(2, 1, 3)
    o_map.set_number(4, 4, 3)
    o_map.set_number(6, 7, 3)

    o_map.set_number(5, 0, 4)
    o_map.set_number(6, 3, 4)
    o_map.set_number(2, 5, 4)
    o_map.set_number(8, 8, 4)

    o_map.set_number(3, 1, 5)
    o_map.set_number(8, 4, 5)
    o_map.set_number(7, 7, 5)

    o_map.set_number(2, 2, 6)
    o_map.set_number(6, 6, 6)

    o_map.set_number(9, 0, 7)
    o_map.set_number(4, 3, 7)
    o_map.set_number(3, 5, 7)
    o_map.set_number(2, 8, 7)

    o_map.set_number(8, 1, 8)
    o_map.set_number(6, 4, 8)
    o_map.set_number(1, 7, 8)

    # Extra
    # o_map.set_number(2, 0, 0)

    # Speculative
    o_map.set_number(7, 0, 3)


    # Another map
    o_map2 = SudokuMap(9, 9, o_color=o_color)
    o_map2.set_number(i_number=5, i_x=0, i_y=0)
    o_map2.set_number(i_number=9, i_x=5, i_y=0)

    o_map2.set_number(i_number=7, i_x=2, i_y=1)
    o_map2.set_number(i_number=2, i_x=7, i_y=1)

    o_map2.set_number(i_number=2, i_x=0, i_y=2)
    o_map2.set_number(i_number=3, i_x=4, i_y=2)
    o_map2.set_number(i_number=1, i_x=5, i_y=2)
    o_map2.set_number(i_number=9, i_x=7, i_y=2)

    o_map2.set_number(i_number=7, i_x=0, i_y=3)
    o_map2.set_number(i_number=1, i_x=2, i_y=3)
    o_map2.set_number(i_number=6, i_x=3, i_y=3)
    o_map2.set_number(i_number=9, i_x=4, i_y=3)
    o_map2.set_number(i_number=4, i_x=8, i_y=3)

    o_map2.set_number(i_number=1, i_x=4, i_y=4)

    o_map2.set_number(i_number=6, i_x=0, i_y=5)
    o_map2.set_number(i_number=7, i_x=4, i_y=5)
    o_map2.set_number(i_number=4, i_x=5, i_y=5)
    o_map2.set_number(i_number=3, i_x=6, i_y=5)
    o_map2.set_number(i_number=1, i_x=8, i_y=5)

    o_map2.set_number(i_number=5, i_x=1, i_y=6)
    o_map2.set_number(i_number=3, i_x=3, i_y=6)
    o_map2.set_number(i_number=6, i_x=4, i_y=6)
    o_map2.set_number(i_number=8, i_x=8, i_y=6)

    o_map2.set_number(i_number=6, i_x=1, i_y=7)
    o_map2.set_number(i_number=7, i_x=6, i_y=7)

    o_map2.set_number(i_number=9, i_x=3, i_y=8)
    o_map2.set_number(i_number=3, i_x=8, i_y=8)

    # Extra help while not implemented the best algorithm
    # =============================================================================================================
    # |     5     ¦ 148       ¦ 48        |     7     ¦     2     ¦     9     | 148       ¦     3     ¦     6     |
    # -------------------------------------------------------------------------------------------------------------
    # | 13489     ¦ 13489     ¦     7     | 48        ¦ 48        ¦     6     | 148       ¦     2     ¦     5     |
    # -------------------------------------------------------------------------------------------------------------
    # |     2     ¦ 48        ¦     6     |     5     ¦     3     ¦     1     | 48        ¦     9     ¦     7     |
    # =============================================================================================================
    # |     7     ¦ 38        ¦     1     |     6     ¦     9     ¦ 358       |     2     ¦ 58        ¦     4     |
    # -------------------------------------------------------------------------------------------------------------
    # | 348       ¦ 2348      ¦ 3458      | 28        ¦     1     ¦ 358       |     6     ¦     7     ¦     9     |
    # -------------------------------------------------------------------------------------------------------------
    # |     6     ¦ 289       ¦ 589       | 28        ¦     7     ¦     4     |     3     ¦ 58        ¦     1     |
    # =============================================================================================================
    # | 14        ¦     5     ¦     2     |     3     ¦     6     ¦     7     |     9     ¦ 14        ¦     8     |
    # -------------------------------------------------------------------------------------------------------------
    # | 3489      ¦     6     ¦ 3489      |     1     ¦ 458       ¦ 58        |     7     ¦ 45        ¦     2     |
    # -------------------------------------------------------------------------------------------------------------
    # | 148       ¦     7     ¦ 48        |     9     ¦ 458       ¦     2     | 145       ¦     6     ¦     3     |
    # =============================================================================================================
    # By best algorithm I mean that the last in the middle vertical quadrant from the top right horizontally,
    # only 5 can be in a column. That clarifies that 5 must go to the other column in last quadrant, first column. Coord 6x8
    # o_map2.set_number(i_number=5, i_x=6, i_y=8)
    # ERROR traces from a bug fixed to mentioned during the code review
    # Surprisingly this fails
    # > Leaving scan for 8 in row1
    # V Scanning for removing 8 in col 4
    # V Removed 8 From: 4 7 [5, 8] Pending: [5]
    # Found 5 From: 4 7
    #
    # > Scanning for removing 5 in row 7
    # > Removed 5 From: 5 7 [5, 8] Pending: [8]
    # > Found 8 From: 5 7
    #
    # > Scanning for removing 8 in row 7
    # > Removed 8 From: 0 7 [3, 8] Pending: [3]
    # > Found 3 From: 0 7

    o_map = o_map2

    print(o_map.get_map_drawing_as_string())

    b_changes_found = True
    while b_changes_found is True:
        b_changes_found = False

        for i_y in range(0, o_map.i_height):
            b_found = o_map.detect_and_remove_a_number_from_possibles_from_a_row(i_y=i_y)
            if b_found is True:
                print(o_map.get_map_drawing_as_string())

        for i_y in range(0, o_map.i_height):
            o_map.o_color.print_label("Scanning quadrants for row " + str(i_y))
            for i_number in range(1, 10):
                for i_x in range(0, o_map.i_width):
                    b_found, i_x_found, i_y_found = o_map.check_if_number_possibles_in_quadrant_is_unique(i_number_to_check=i_number, i_x=i_x, i_y=i_y)
                    if b_found is True:
                        # Search again
                        b_changes_found = True
                        o_map.remove_a_number_from_possibles_in_a_row(i_number_to_remove=i_number, i_y=i_y_found)
                        o_map.remove_a_number_from_possibles_in_a_column(i_number_to_remove=i_number, i_x=i_x_found)

                    b_found, i_x_found, i_y_found = o_map.check_if_number_possibles_in_row_is_unique(i_number_to_check=i_number, i_y=i_y)
                    if b_found is True:
                        b_changes_found = True
                        o_map.remove_a_number_from_possibles_in_a_column(i_number_to_remove=i_number, i_x=i_x_found)

                if b_changes_found is True:
                    print(o_map.get_map_drawing_as_string())

    # @TODO: Implement check if number in quadrant can only go to a column, to remove the non possible in that column from another quadrant
    # @TODO: Implement check if in a line only one number can go to a column.

    print(o_map.get_map_drawing_as_string())
    print(o_map.get_map_drawing_of_possibles_as_string())

The color library lib/colorutils.py:

from colorama import Fore, Back, Style , init


class ColorUtils:

    def __init__(self):
        # For Colorama on Windows
        init()

    def print_error(self, m_text, s_end="\n"):
        """
        Prints errors in Red.
        :param s_text:
        :return:
        """

        # If they pass numbers
        s_text = str(m_text)

        print(Fore.RED + s_text)
        print(Style.RESET_ALL, end=s_end)

    def print_success(self, m_text, s_end="\n"):
        """
        Prints errors in Green.
        :param s_text:
        :return:
        """

        # If they pass numbers
        s_text = str(m_text)
        print(Fore.GREEN + s_text)
        print(Style.RESET_ALL, end=s_end)

    def color_success(self, m_text):
        """
        Colors only this
        :param m_text:
        :return:
        """

        s_text = str(m_text)
        return Fore.GREEN + s_text + Fore.RESET

    def color_black(self, m_text):
        s_text = str(m_text)
        return Fore.BLACK + s_text + Fore.RESET

    def color_blue(self, m_text):
        s_text = str(m_text)
        return Fore.BLUE + s_text + Fore.RESET

    def color_red(self, m_text):
        s_text = str(m_text)
        return Fore.RED + s_text + Fore.RESET

    def color_yellow(self, m_text):
        s_text = str(m_text)
        return Fore.YELLOW + s_text + Fore.RESET

    def color_magenta(self, m_text):
        s_text = str(m_text)
        return Fore.MAGENTA + s_text + Fore.RESET

    def print_label(self, m_text, s_end="\n"):
        """
        Prints a label and not the end line
        :param s_text:
        :return:
        """

        # If they pass numbers
        s_text = str(m_text)

        print(Fore.BLUE + s_text, end="")
        print(Style.RESET_ALL, end=s_end)

    def return_text_blue(self, s_text):
        """
        Restuns a Text
        :param s_text:
        :return: String
        """
        s_text_return = Fore.BLUE + s_text + Style.RESET_ALL
        return s_text_return

News from the blog 2022-04-22

Media/Press

I was interviewed by Radio America Barcelona, in their studios in Barcelona.

RAB is a radio for the Catalan diaspora and expats.

The interview was broadcasted by Twitch and can be watched. It’s in Catalan language:

https://www.twitch.tv/videos/1448895585

You can follow them:

Sant Jordi discounted books (promo)

Tomorrow 23th of April is Sant Jordi (Saint George), the patron of Catalonia, and the Catalan traditional celebration consist in this day women gifting a book to the men they love and men gifting a rose to the women they love.

I created a voucher for all my Python books, so, since during 23th of April you can acquire the four books per $10 USD in total.

This voucher is limited to 100 sales.

https://leanpub.com/b/python3all/c/SANTJORDI2022

Python 3

I wrote an article with a Python 3 code that shows the length for file names in Linux ext3 and ext4 Filesystems and in ZFS Pools Filesystem.

I show basically how ASCii characters over 127 are encoded, reducing the maximum length of 255 bytes for the filename.

ZFS

I’ve updated an article explaining how to create a ZPool raidz (RAID 5 equivalent) from three loop devices based on local files.

Thanks to those that bought my ZFS book this month. :)

HTML and JavaScript

I wrote a super simple code to hide the <p> using jQuery and JavaScript.

Free books

https://books.goalkicker.com/

Books I bought

This month I bought these books.

Firewall

I continued to block any Russian or Belarus Ip Address that connects to the blog.

I also started to block entire ranges of Ip’s from Digital Ocean, as many attacks come from Servers in their infrastructure.

Despite blocking tens of thousands of Ip Addresses, the number of visitors keep growing.

My Health

Thanks to my strict discipline I managed to recover super well and I’m healthier than before and guided by the satisfied doctors we removed two daily medicines.

I started a new medicine that is for the final phase of my recuperation, which doctors expect to be completely. In fact I’m much more healthier than I was before going to the hospital.

Humor

Will AI take the world?
Sadly, true history. The responsibility to deliver is from all of us.

A super simple HTML jQuery JavaScript to hide a text

Some of my students are working on the Front End part, so I help them with JavaScript and jQuery.

Even if I’m super strong in Back End I’m not bad at JavaScript Front End.

So this super simple code will hide the paragraphs <p> using the hide animation from jQuery.

<html>
    <head>
    	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    </head>
    <body>
        <p>Text to hide</p>
        <input type="button" id="button" value="Hide the text" onclick='$("p").hide(1000);'>
    </body>
</html>

I think learning should be taken by steps, and starting by the most simple.

Testing length of Linux ext3/ext4 and ZFS file names with Python 3

Last Update: 2022-04-16 15:22

So, I was working on a project and i wanted to test how long a file can be.

The resources I checked, and also the Kernel and C source I reviewed, were pointing that effectively the limit is 255 characters.

But I had one doubt… given that Python 3 String are UTF-8, and that Linux encode by default is UTF-8, will I be able to use 255 characters length, or this will be reduced as the Strings will be encoded as UTF-8 or Unicode?.

So I did a small Proof of Concept.

For the filenames I grabbed a fragment of the translation to English of the book epic book “Tirant lo Blanc” (The White Knight), one of the first books written in Catalan language and the first chivalry novel known in the world. You can download it for free it in:

https://onlinebooks.library.upenn.edu/webbin/gutbook/lookup?num=378

Python Code:

from carleslibs.fileutils import FileUtils
from colorama import Fore, Back, Style , init


class ColorUtils:

    def __init__(self):
        # For Colorama on Windows
        init()

    def print_error(self, s_text, s_end="\n"):
        """
        Prints errors in Red.
        :param s_text:
        :return:
        """
        print(Fore.RED + s_text)
        print(Style.RESET_ALL, end=s_end)

    def print_success(self, s_text, s_end="\n"):
        """
        Prints errors in Green.
        :param s_text:
        :return:
        """
        print(Fore.GREEN + s_text)
        print(Style.RESET_ALL, end=s_end)

    def print_label(self, s_text, s_end="\n"):
        """
        Prints a label and not the end line
        :param s_text:
        :return:
        """
        print(Fore.BLUE + s_text, end="")
        print(Style.RESET_ALL, end=s_end)

    def return_text_blue(self, s_text):
        """
        Restuns a Text
        :param s_text:
        :return: String
        """
        s_text_return = Fore.BLUE + s_text + Style.RESET_ALL
        return s_text_return


if __name__ == "__main__":

    o_color = ColorUtils()
    o_file = FileUtils()

    s_text = "In the fertile, rich and lovely island of England there lived a most valiant knight, noble by his lineage and much more for his "
    s_text += "courage.  In his great wisdom and ingenuity he had served the profession of chivalry for many years and with a great deal of honor, "
    s_text += "and his fame was widely known throughout the world.  His name was Count William of Warwick.  This was a very strong knight "
    s_text += "who, in his virile youth, had practiced the use of arms, following wars on sea as well as land, and he had brought many "
    s_text += "battles to a successful conclusion."

    o_color.print_label("Erasure Code project by Carles Mateo")
    print()
    print("Task 237 - Proof of Concep of Long File Names, encoded is ASCii, in Linux ext3 and ext4 Filesystems")
    print()
    print("Using as a sample a text based on the translation of Tirant Lo Blanc")
    print("Sample text used:")
    print("-"*30)
    print(s_text)
    print("-" * 30)
    print()
    print("This test uses the OpenSource libraries from Carles Mateo carleslibs")
    print()
    print("Initiating tests")
    print("================")

    s_dir = "task237_tests"

    if o_file.folder_exists(s_dir) is True:
        o_color.print_success("Directory " + s_dir + " already existed, skipping creation")
    else:
        b_success = o_file.create_folder(s_dir)
        if b_success is True:
            o_color.print_success("Directory " + s_dir + " created successfully")
        else:
            o_color.print_error("Directory " + s_dir + " creation failed")
            exit(1)

    for i_length in range(200, 512, 1):
        s_filename = s_dir + "/" + s_text[0:i_length]
        b_success = o_file.write(s_file=s_filename, s_text=s_text)
        s_output = "Writing file length: "
        print(s_output, end="")
        o_color.print_label(str(i_length).rjust(3), s_end="")
        print(" file name: ", end="")
        o_color.print_label(s_filename, s_end="")
        print(": ", end="")
        if b_success is False:
            o_color.print_error("failed", s_end="\n")
            exit(0)
        else:
            o_color.print_success("success", s_end="\n")

    # Note: up to 255 work, 256 fails

I tried this in an Ubuntu Virtual Box VM.

As part of my tests I tried to add a non typical character in English, ASCii >127, like Ç.

When I use ASCii character < 128 (0 to 127) for the filenames in ext3/ext4 and in a ZFS Pool, I can use 255 positions. But when I add characters that are not typical, like the Catalan ç Ç or accents Àí the space available is reduced, suggesting the characters are being encoded:

Ç has value 128 in the ASCii table and ç 135.

I used my Open Source carleslibs and the package colorama.

Install them with:

pip3 install carleslibs colorama

Some tips for beginners at Elden Ring

Last Update: 2022-05-16 08:03 Irish Time

I played it on Xbox, so these tips are for Xbox.

Runes

When you kill enemies you get runes.

These are used for increasing your stats or weapons stats.

When you die you’ll lose the runes.

You can recover them if you go to the place where you died. You’ll see them shining on the floor. The place you died is marked on the map.

You’ll not lose things like the map, flowers you picked, etc…

You don’t lose your stats.

Horseback mount

After you get the Spectral Steed Whistle bind it to your belt through the inventory or pouch. It looks like a golden ring.

Then you’ll be able to summit it by selecting it (keypad down changes the item selected from the belt) and pressing X.

If you jump over some special spinning wind zones you’ll fly to very high distances.

You can run/sprint!

That’s something I didn’t see in the controls and I was being killed many many times, so I figured it out there has to be a way to sprint.

You can run by pressing the dodge button, B in XBox. Press B and hold it and move to any direction.

You can also use it when you are mounting Torrent, your horse.

Rolling dodges some attacks

Some attacks are avoided by rolling.

Mentioned by my friend Cian O’Brien.

Weight

You can have many items with you, but when you equip them the weight is important.

If you equip too much you’ll be unable jump or roll.

You can have several weapons with you and just equip what you need at each moment (before engaging combat).

Golden runes

You’ll find golden runes that will be kept in your inventory. They are consumables and you can use them to get instant amount of fragments.

Summon Ashes spirits

You’ll find spirits that you can summon in points of interest and to fight bosses.

Flasks

You can balance the number of flasks you have, and increase the number of them by using golden seeds at the sites of grace and the amount of life they replenish if you have the sacred tears.

Sort Chest

At the sites of grace you can sort your chest, and move things from your inventory to the chest and vice versa.

That’s ideal for reducing the items you carry until the appropriate moment to sell the goods.

Chests protected by soldiers in cars

You will find some cars protected by soldiers or big monsters.

It took me many time to realize that in the back of them they have a chest.

Jump and open the chest. Good weapons will become handy at an early stage.

Use the shield

Pressing LB if you have the shield on the left hand will raise the shield and keep it lifted.

This will protect you from incoming attacks.

I killed many enemies and bosses without using the shield, and it was a bit of a pain.

Sekiro style, dying and dying. Raising the shield I was able to beat impossible-to-beat-before bosses easily.

Head Skull

There are head skulls in the map, with bright eyes.

If you crack them hitting with your weapon, you’ll get a golden rune.

You can sell the golden runes to merchants or use them at any time and you’ll get runes. You may have a treasure enough to level up and you may not known.

You don’t loss the golden runes when you die.

Cycling items in your bag

You can bind several items to your back and cycle them pressing the down key on the pad.

If you bind the healing potion to the first slot in your bag if you hold down the cycle key it will always go back to the first item (thanks Cian!)

My friend Michela says: Carles I suggest if i may put the ring in backup pouch because if you map on the menu where healing potions are you risk to call  the horse when u wanna heal

:smile:

If you press and hold Y you’ll see the actions that you have mapped in the pouch. So, it’s also a quick way to access these.

Eliminating groups

Will restore a flask, so you will be able to use another crimson tears. This is capped to your max number of flasks.

Torch

You can buy a torch and use it. Useful for caves and dark places. Equip it as a weapon and just select it.

Traveling to the Round Table

When the requirements are met and you travel to the round table, you can go back by opening the map and zooming out. You’ll see then the round table.

Craft bombs

You can craft bombs if you have recipients to hold them.

Bombs are very useful against some bosses.

In order to craft you’ll need to buy a crafting kit.

Medallions

Michela also recommends: Bosses use to drop medallions. You can equip them in the medallion/s slot.

Use Magic

Once you have a spell you can go to a site of grace and select Memorize Spell.

Then with the up path you can switch.

You need to have a casting tool in any hand and press the attack button for that hand in order to perform the sorcery.

I found my first staff in the Sellia Crystal cave, after killing several of these digging blue crystal enemeies, which is a zone where you travel after you open a chest in a cave in Dragon-Buirnt ruins which you access going down stairs in the runes that have the zombies, where the big dragon is. It’s the Digger’s staff.

The centipedes in that area are very hard but I found them very vulnerable to fire bombs that you can craft and to throwing curved knives that you can buy in merchants.

After you defeat a boss in a cave next to a gigantic plant, you can become Sorceress Sellen’s protegee and she will teach you magic (she sell scrolls).

You can give her the academy scroll, IDK if this is good or bad.

Show glinstone projectiles without aiming

You don’t need to have the focus over an enemy. If you’re looking at the right direction you can just shoot using your staff, and the projectile will go to the enemy automatically.

Use Incantations

You can use incantations, that are another type of spell.

For doing incantations you need a Sacred Seal.

You can buy one in the two commerce women in the round table.

I discovered this after 82 hours of gameplay.

The incantations are based of faith.

Kick stairs

Like in Sekiro Shadows die Twice, after you get to certain parts you will be able to kick a stair so you’ll not need to do all the round to get to a boss.

Try different weapons

Different weapons have different behavior that you may find very convenient.

I recommend you to try different of them.

Some shields parry, which means that if you press the LT action button in the precise moment an enemy attacks, they will become vulnerable for a moment. You can then perform a critical with RB.

Great Runes

When you defeat one of the mega bosses, the demi gods, you will get a great rune.

You can use the great runes after restoring their power (only once) in a tower.

Then you equip a great rune in a site of grace.

They grant some buffs, but to get the most of them you have to use a Rune Arc.

These are dropped by rats and big rats.

Using a Rune Arc when having a Great Rune active gives you buff, like increased stats, until you die. Then you need another Arc.

I used an Arc for the first time when I was level 77.

Hashes of war can be duplicated

The hashes of war give special effects to the weapons. To be honest I have been afraid of using it and ruining a good weapon effect, or losing the ash.

But Ashes of war can be duplicated if you have the proper ingredient, in the smith in the Round Table.

I found the first nomadic merchant selling one of these ingredients just after South Raya Lucaria Gate, coming back after traversing the seal, from Main Academy Gate, just walking through the seal, without examining it, and after killing some packs of wolves.

I found this at level 77…

After 120 hours played I was level 83.

After 150 hours played I was level 111 and I had not yet used a single Ash of war.

Some attacks can be charged

Some attacks, like Loretta’s great arrow, can be charged by holding the fire button.

When it’s fired at its max charge level, it will deal more damage for the same FP cost.

Don’t roll over poison

In some caves you’ll find poison or rott on the floor. Don’t roll over trying to advance faster. That will cover your clothes with the poison and the disease will continue after you leave the infected zone. Just run or walk.

Plants in the caves allow you to level up the spirit ashes

The plants that are in the caves, allow to level up the spirit ashes.

Front End (Beginners): Check if an AJAX request is made

I created this little video to show how to use the Web Developer Tools to check if jQuery AJAX request is being made.

This is to help beginners in their journey learning to program Front End.

Please be aware that the page I used makes their own calls for marketing purposes. I cleared several times the history to show what is the real AJAX request we are doing.

Note: The recording program did not capture correctly the mouse position and this video has no sound.

The page I shown is:

https://www.w3schools.com/jquery/tryit.asp?filename=tryjquery_ajax_ajax

And some more info about jQuery .ajax method:

https://www.w3schools.com/jquery/ajax_ajax.asp

Normally you can try your HTML and JavaScript from your browser, opening the HTML file. However there are Security restrictions when it applies to AJAX requests or trying to access a local file from the browser.

You’ll need to run your JavaScript from an HTML page server from a Web Server. You can install apache if you use Linux, or WAMP if you use Windows or MAMP (Mac Apache MySQL PHP) if you use Mac or if you know how to you can create a Docker Container.

When you’re programming and your code does not trigger, pay attention to errors:

Top right, red circle, shows one error

News from the blog 2022-03-22

Support to Ukraine

I’m Catalan. In 1936 the fascist military leaded by franco raised in arms against the elected government of the Spanish Republic. The Italian and nazi German fascist in power bombed the Catalan population. Hundreds of thousands of innocent citizens were assassinated and millions of Catalan and Spaniards had to exile. The sons of those that were ruling with the dictator have been insisting in naming it a “civil war”, but it was the military lead by a fascist, revolting against the legitimate Republic and ending a democracy.

The dictatorship lasted until 1975, when the dictator died in the bed. The effects of the repression never abandoned Catalonia, and nowadays in Catalonia people is still detained by the Spanish police for talking the Catalan language in front of them, and our Parliament decisions are cancelled by the Spanish courts, for example to force the exit of a President of Catalonia that they didn’t like, or to force the Catalan schools to teach 25% of the time in Spanish attacking the Catalan teaching system.

During WW2 millions of Jews were mass murdered, also people from all the nations were assassinated.

Russian population suffered a lot also fighting the nazis.

Now we have to see how Russia’s army is invading Ukraine and murdering innocent citizens.

That’s horrible.

I know Engineers from Ukraine. Those guys were doing great building wealthy based on knowledge and working well for companies across the world. Now these people are being killed or Engineers, amongst all the brave population, are arming themselves to fight the invasion. Shells destroy beautiful cities and population are starving, and young soldiers from both sides will never be seen again by their mothers.

I wrote a small article on how to identify and block in the Ubuntu firewall the Ip’s from Russia and Belarus until Russia leaves Ukraine.

Let music play in solidarity with Ukraine. First is a Catalan group. Second is a famous Irish band in this epic song dedicated to the brave International Brigades, volunteers that fought the fascism in Spain and in Catalonia trying to make a better world.

The Blog

I’ve updated the SSL Certificate. The previous one I bought was issued for two years, and I renewed as it was due to expire.

I wrote a short article about how to update the SSL Certificates for Apache 2 in Ubuntu 20.04.

Articles

I published a small Python script to show the local datetime and the Unix Epoch Time.

Open Source

carleslibs

On the 6th of January I released carleslibs v.1.0.7

https://pypi.org/project/carleslibs/1.0.7/

The new version contains these improvements:

  • Modified OsUtils.get_total_and_free_space_in_gib() to return float instead of Integer.
  • Added HashUtils class with md5 for unicode Strings.
    • Produces the same as md5sum Linux tool.
  • Created FileUtils.create_folders() which creates all the subfolders in the path deep.
  • Unit Testing:
    • Added test_get_inodes_in_use_and_free() to test_osutils.py
    • Added two tests more to test_osutils.py
    • Added test for version.py
    • Tests for HashUtils class.

My books

Python 3 Exercises for Beginners

I have updated the book, offering solution to exercises 11.1, 11.2 (simple and encapsulated in a function) and I’ve created exercise 11.3.

If you purchased the book before, you can download any update for free.

Amazon AWS

I got an offer by a super editorial to publish my book Automating and Provisioning with Amazon Python 3 SDK boto3.

Honestly, my ego was flattered. It is a lot of reputation.

Although in the past I got an offer from another monstrously big editorial to publish world wide my book Python 3 Combat Guide and I also rejected, and an offer from a digital learning platform to create an interactive course from this same book.

I’ve rejected it again this time.

If you are curious, this is what I answered to them:

Hi XXXX,

I'm well, thank you. I hope you are doing well too.

Thanks for taking the time to explain your conditions to me.

I feel flattered by your editorial thinking about me. I respect your brand, as I mentioned, as I own several of your titles.

However, I have to refuse your offer.

Is not the first time an editor has offered to publish one or more of my books. For all over the world, with much higher economic expectations.

I'll tell you why I love being at LeanPub:

1- I own the rights. All of them.
2- I can publish updates, and my readers get them for free. As I add new materials, the value is maximized for my readers.
3- I get 80% of the royalties.
4- If a reader is not happy, they can return the book for 60 days.
5- I can create vouchers and give a discount to certain readers, or give for free to people that are poor and are trying to get a career in Engineering.

The community of readers are very honest, and I only got 2 returns. One of them I think was from an editorial that purchased the book, evaluated it, and they contacted me to publish it, and after I rejected they applied for the refund.

I teach classes, and I charge 125 EUR per hour. I can make much more by my side than the one time payment you offer. The compensation for the video seems really obsolete.

Also, I could be using Amazon self publishing, which also brings bigger margins than you.

So many thanks for your offer. I thought about it because of the reputation, but I already have a reputation. I've thousands of visits to my tech blog, and because of the higher royalties, even if I sell less books through LeanPub it is much more rewarding.

Thanks again and have a lovely day and rest of the week.


Best,
Carles

The provisioning in Amazon AWS through their SDK is a book I’m particularly proud, as it empowers the developers so much. And I provide source code so they can go from zero to hero, in a moment. Amazon should provide a project sample as I do, not difficult to follow documentation.

Teaching / Mentoring

As I was requested, I’ve been offering advice and having virtual coffees with some people that recently started their journey to become Software Engineers and wanted some guidance and advice.

It has been great seeing people putting passion and studying hard to make a better future for themselves and for their families.

I’ll probably add to the blog more contents for beginners, although it will continue being a blog dedicated to extreme IT, and to super cool Engineering skills and troubleshooting.

For my regular students I have a discord space where we can talk and they can meet new friends studying or working in Engineering.

Free Resources

This github link provides many free books in multiple languages:

https://github.com/EbookFoundation/free-programming-books

Tricks

Zoom can zoom the view. So if they are sharing their screen, and font is too small, you can give a relax to your eyes by using Zoom’s zoom feature. It is located in View.

My health

After being in the hospital in December 2021, with risk for my life, and after my incredible recuperation, I’ve got the good news that I don’t need anymore 2 of the 3 medicines I was taking in a daily basis. It looks well through a completely recovery thanks to my discipline, doing sport every day several times, and the fantastic Catalan doctors that are supporting me so well.

Since they found what was failing in me, and after the emergency treatments I started to sleep really well. All night. That’s a privilege that I didn’t have for long long time.

Humor

Sad but true history. How many super talented Engineers have been hired and then they were given a shitty laptop/workstation super slow? That happened to me when I was hired by Volkswagen IT: gedas. I was creating projects for very big companies and I calculated that I was wasting 2 hours of my time compiling. The computer did not had enough RAM and was using swap.

JavaScript fun (or not)

Yes, this works like this.

You can try yourself:

<html>
<body>
<script>
    console.log("11" + 1)
    console.log("11" - 1)
</script>
</body>
</html>

As you can see if you open the Browser Developer tools (in Linux and Windows press F12 key):

Firsts steps programming in Python 3 with PyCharm and Git

This is a very simple guide for beginners, to benefit from using this amazing Python IDE with Git.

You may think that it’s a bit difficult and why you should use it. The first reason I would tell is because it marks the errors, not just syntax errors also if you have typos or you forgot an import or you forgot to declare a variable, has autocomplete, will mark when you don’t follow standards, etc… If you want to be a professional, this is the tool that most Python professional developers and companies use.

Go to Jetbrains page and from there select Developer Tools and PyCharm.

You can use this url to go directly:

https://www.jetbrains.com/pycharm/

PyCharm Community Edition is completely free.

If you don’t have it, create an account in Gitlab. I recommend you to use Gitlab over Github.

Before continuing you’ll have to validate your account by clicking the email that Gitlab sent you.

Create a New project, by pressing the blue button with this label.

Choose Create blank project.

If you select Public everybody will be able to see your code, I recommend you to start with a Private project.

Let checked Initialize repository with a README as this will save you plenty of time and will allow you to start working immediately.

The project will be created for you.

Hit the Clone blue button and it will provide two methods to clone your git repository. You will copy the Clone with HTTPS url.

There are different ways to clone a new project.

Assuming you already have one opened go to Main Menu > Git > Clone

In the URL field paste the URL you copied from Gitlab, in my case:

https://gitlab.com/carles.mateo/carles-mateo-demo-project.git

And press the blue botton Clone.

If you don’t have Git installed, PyCharm will tell you in red, and will allow you to install it for you. It is a very convenient way to install it, specially if you use Windows.

In Ubuntu or Debian Linux installing git is so easy as typing from the Terminal:

sudo apt install git

If it’s your first time PyCharm will ask you for your credentials in Gitlab, so your username or email, and your password.

As it is you own new project and it’s empty, you can trust it. Hit Trust Project.

A window with tips will appear, you can close it.

As you see your project is empty. Only has a the default README.md file created by Gitlab.

Creating new Python files

On the top left, over the name of your project folder press with the Right button of the mouse and select New > Python File

Enter the name of the file, I choose helloworld and press Enter.

You will be asked if you want to add automatically the file to Git. This refers to your local Git repository. Say Yes and you will see the file changing from color Red, which means it is not added to the repository to Green. Green means that is added, but has never been committed.

Just add this line to helloworld.py

print("Hello World")

Note: I made a typo and I wrote “helloworl.py” instead of “helloworld.py”, I will show how to fix this later.

Now, press over the file name over the top left, with the Right button of the mouse, and select Run helloworl (In your case will be Run helloword)

Note: To be able to run a Python program, you need to have Python installed.

Note2: Do not start the name of your file by test as PyCharm will consider that you are writing Unit Testing Code, and will not show the option to run the code, but to run with pytest (if you have pytest framework installed).

And you see on the bottom how it worked and printed “Hello World”.

To rename the file you’ll click over it with the Right button of the mouse and select Refactor > Rename

Type the correct name, in my case “helloworld.py” and for the Scope select: Current File.

PyCharm is so powerful that can search for your entire project to see if other files are using that file and will update the code to reflect the file name change. By indicating Scope: Current File we are telling that the change affects only to this file, and by not marking Search for references and not marking Search in comments and strings you are indicating to PyCharm that it should not search in code and comments to update them with the new file. This option is really useful when you get more confident with programming.

Perfect. Now you want to commit the file to the repository and Push.

The first time PyCharm will ask you how people will know you, that is, your visible name in the Git History of Commits, and your email. You can mark to use these properties locally. This is basically because Git needs to know how to identify you. Consider that Git was created to work with Teams, so everybody should know who made what. Add your friendly name, like “Carles Mateo” and your email. I recommend you to check to use this info for Git globally (all the projects in your computer).

When you commit you’re expected to provide a Commit Message. As Git is used by teams, to work on the same code, try to explain briefly what your changes consisted on.

If you press Commit, you will see how helloworld.py is no longer in color green, now is black. This means that the file is up to date respect our local Git.

Go to the Main Menu and select Git > Push.

You’ll see a message in the bottom indicating that your code has been pushed to the repository.

Now, you can refresh your browser, and you’ll see the changes in Gitlab:

PyCharm vs VSCode

I got asked what are the advantage from PyCharm respect VSCode.

I may refer to this excellent comparison:

While VSCode has some great support for Python coding with the ‘Python’ plugin by Microsoft, PyCharm is truly designed for Python development and it shows.

https://tangenttechnologies.ca/blog/pycharm-vs-vscode/

https://tangenttechnologies.ca/blog/pycharm-vs-vscode/

Renewing a SSL Certificate for Apache2 in Ubuntu 20.04

First you have to generate a new csr and key files.

It is not recommended to reuse your old CSR file.

openssl req -new -newkey rsa:2048 -nodes -keyout blog_carles_mateo_com_2022.key -out blog_carlesmateo_com_2022.csr

As you can see I used the name of the domain and the year for the new files to be generated to easily distinguish them.

When you’re asked for the password, in the additional fields, keep that password safe in case you need the Cert to be reissued to you.

You’ll need to submit the CSR file to your SSL provider. They will return you the CRT and the CA-BUNDLE files.

Edit your Apache config file for the SSL site.

For example:

/etc/apache2/sites-enabled/11-https-blog-carlesmateo-com.conf

Your conf file will look similar to this:

<VirtualHost *:443>
	ServerAdmin webmaster@yourdomain.cat

	DocumentRoot /opt/sites/www/blog.carlesmateo.com
	ServerName blog.carlesmateo.com
        SSLEngine on
        SSLCertificateFile /opt/sites/certs/2022/blog_carlesmateo_com_2022.crt
        SSLCertificateKeyFile /opt/sites/certs/2022/blog_carlesmateo_com_2022.key
        SSLCertificateChainFile /opt/sites/certs/2022/blog_carlesmateo_com_2022.ca-bundle
...

Before restarting Apache2, test the configuration for syntax errors with:

apache2ctl -t

If all is good, restart your Web Server with:

service apache2 restart

With a browser, verify that the information of the domain is right. I recommend you to check in Firefox and Chrome at least.

A simple Python script to get the date and time in Unix Epoch and in local time

The Unix Epoch is the time, in seconds, that has passed since 1970-01-01 00:00:00 UTC.

#!/usr/bin/env python3

#
# Date time methods
#
# Author: Carles Mateo
# Creation Date: 2019-11-20 17:23 IST
# Description: Class to return Date, datetime, Unix EPOCH timestamp
#

import datetime
import time


class DateTimeUtils:

    def get_unix_epoch(self):
        """
        Will return the EPOCH Time. For convenience is returned as String
        :return:
        """
        s_now_epoch = str(int(time.time()))

        return s_now_epoch

    def get_datetime(self, b_milliseconds=False):
        """
        Return the datetime with miliseconds in format YYYY-MM-DD HH:MM:SS.xxxxx
        or without milliseconds as YYYY-MM-DD HH:MM:SS"""
        if b_milliseconds is True:
            s_now = str(datetime.datetime.now())
        else:
            s_now = str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))

        return s_now


def main():
    o_datetime = DateTimeUtils()
    s_now_epoch = o_datetime.get_unix_epoch()
    s_now_date = o_datetime.get_datetime()

    print(s_now_epoch)
    print(s_now_date)


main()

You can also get the code from:

https://gitlab.com/carles.mateo/blog.carlesmateo.com-source-code/