Source code for redirectory.libs_int.importers.csv_importer

"""
CSV Importer
============

The CSV Importer takes care of importing CSV files containing Redirect Rules
and adding them into the SQL database of the management pod.

The behaviour:

1. If a rule in the CSV already exists it is going to be ignored.
2. If a syntax/parsing error occurs somewhere in the CSV file the whole import
   is marked as **failed** and all of the changes to the database are roll backed.

"""
import io
import csv

from werkzeug.datastructures import FileStorage
from kubi_ecs_logger import Logger, Severity

from redirectory.libs_int.database import add_redirect_rule, DatabaseManager


[docs]class CSVImporter: """ A new **CSVImporter** is created for every import and the data of the CSV file is passed as a parameter in the constructor of the class. """ csv_reader = None """Reader object used to parse the CSV file""" data_template = { "domain": None, "domain_is_regex": None, "path": None, "path_is_regex": None, "destination": None, "destination_is_rewrite": None, "weight": None } """This is the template that the CSV is checked against. Every row of the CSV must match this template otherwise the whole import will fail""" def __init__(self, csv_byte_file_in: FileStorage): assert csv_byte_file_in.mimetype == "text/csv", "The file must be of type CSV." # Get String IO object from encoded stream csv_string = csv_byte_file_in.stream.read().decode("utf-8") csv_string_file = io.StringIO(csv_string) # Create a new dialect for parsing csv_dialect_name = "csv_redirectory_dialect" csv.register_dialect(csv_dialect_name, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL, skipinitialspace=True) # Parse the file self.csv_reader = csv.reader(csv_string_file, dialect=csv_dialect_name) # Get columns and validate self.columns = next(iter(self.csv_reader)) self._validate_columns()
[docs] def import_into_db(self): """ Imports all the rules in the given csv file into the database as RedirectRules. If a rule is a duplicate it will be skipped. If there is an error in parsing the csv then all the changes will be roll backed and the whole import will be marked as fail. """ db_session = DatabaseManager().get_session() try: row_counter = 1 for row in self.csv_reader: row_counter += 1 assert len(row) == len(self.columns), f"Entry at line: {row_counter} has different amount of " \ f"arguments than expected. Expected: {len(self.columns)} instead got: {len(row)}" self.data_template["domain"] = row[0] self.data_template["domain_is_regex"] = self._get_bool_from_str(row[1]) self.data_template["path"] = row[2] self.data_template["path_is_regex"] = self._get_bool_from_str(row[3]) self.data_template["destination"] = row[4] self.data_template["destination_is_rewrite"] = self._get_bool_from_str(row[5]) self.data_template["weight"] = int(row[6]) result = add_redirect_rule(db_session, **self.data_template, commit=False) if isinstance(result, int) and result == 2: # 2 means already exists raise AssertionError except AssertionError as e: Logger() \ .event(category="import", action="import failed") \ .error(message=str(e)) \ .out(severity=Severity.ERROR) db_session.rollback() except Exception as e: Logger() \ .event(category="import", action="import failed") \ .error(message=str(e)) \ .out(severity=Severity.CRITICAL) db_session.rollback() else: Logger() \ .event(category="import", action="import successful", dataset=f"Rules added from import: {row_counter - 1}") db_session.commit() DatabaseManager().return_session(db_session)
def _validate_columns(self): """ Validates that all the columns in the CSV file are according to the data template Raises: assertionError if something doesn't match """ assert len(self.data_template) == len(self.columns), f"Invalid number of columns. " \ f"Expected {len(self.data_template)} got {len(self.columns)}" for valid_column in self.data_template.keys(): assert valid_column in self.columns, f"{valid_column} is a required column" @staticmethod def _get_bool_from_str(string: str) -> bool: """ Simple conversion of a string to a boolean If the string is truthful e.g. 1 or true then True will be returned If it is anything else then a False is returned Args: string: the string to convert from Returns: the boolean representation of the string """ return string.lower() in ["1", "true"]