Ejemplo n.º 1
0
    def __init__(self, xfile, catfile='cats.json'):
        self.filename = xfile

        self.catman = CategoryManager(catfile)

        self.expenses = list()
        self.figures = list()
    def __init__(self, database):
        """ DatabaseUpdater constructor.

        Creates instances of table manager classes.
        Runs _run() method.

        """
        self.product_manager = ProductManager(database)
        self.category_manager = CategoryManager(database)
        self.product_category_manager = ProductCategoryManager(database)
        self.store_manager = StoreManager(database)
        self.product_store_manager = ProductStoreManager(database)

        self._run(database)
Ejemplo n.º 3
0
    def __init__(self, database):
        """ DatabaseFiller constructor.

        For each category and for each nutrition grade, retrieves
        corresponding products from Open Food Facts API and adds them to
        local database.

        """
        # self.database = database
        # Creates instances of table manager classes
        self.product_manager = ProductManager(database)
        self.category_manager = CategoryManager(database)
        self.product_category_manager = ProductCategoryManager(database)
        self.store_manager = StoreManager(database)
        self.product_store_manager = ProductStoreManager(database)

        for nutrition_grade in nutrition_grades:
            for category in tag_categories:
                products = self._get_products(category, nutrition_grade)
                self._fill_db(products)
class DatabaseUpdater:
    """ Sets DatabaseUpdater class.

    Consists of 14 private methods :
        - __init__()
        - _run()
        - _get_products_codes()
        - _get_OFF_product()
        - _update_new_information()
        - _update_categories_information()
        - _get_local_product_categories_information()
        - _remove_obsolete_categories()
        - _add_new_categories()
        - _update_stores_information()
        - _get_local_product_stores_information()
        - _remove_obsolete_stores()
        - _add_new_stores()
        - _save_update_date()

    """
    def __init__(self, database):
        """ DatabaseUpdater constructor.

        Creates instances of table manager classes.
        Runs _run() method.

        """
        self.product_manager = ProductManager(database)
        self.category_manager = CategoryManager(database)
        self.product_category_manager = ProductCategoryManager(database)
        self.store_manager = StoreManager(database)
        self.product_store_manager = ProductStoreManager(database)

        self._run(database)

    def _run(self, database):
        """ Manages database update.

        For each product of local database, checks information &
        updates if necessary.
        Saves update date.

        """
        codes = self._get_products_codes(database)

        for i in range(len(codes.all())):
            try:
                self._get_OFF_product(codes, i)
                local_product = self.product_manager.\
                    select_product_information(self.OFF_code)
                self._update_new_information(local_product)
                self._update_categories_information(local_product)
                self._update_stores_information(local_product)
            except KeyError as e:
                print('Aïe, KeyError : ', e, file=open('print_log.txt', 'a'))

        self._save_update_date()

    def _get_products_codes(self, database):
        """ Manages product codes retrieving.

        Returns 'codes' object containing product id for all products of
        the local database.

        """
        codes = database.query('''SELECT Product.product_id FROM Product''')
        return codes

    def _get_OFF_product(self, codes, i):
        """ Manages product information retrieving.

        For a given product code, collects product information from Open
        Food Facts API using get() method from request library.

        """
        response = get(f'''https://fr.openfoodfacts.org/api/v0/product/\
{codes[i]['product_id']}.json''')
        data = response.json()
        OFF_product = data['product']

        self.OFF_code = OFF_product['code']
        self.OFF_name = OFF_product['product_name'].strip().capitalize()
        self.OFF_description = OFF_product['generic_name'].capitalize()
        OFF_brands = (OFF_product['brands']).split(',')
        self.OFF_brand = OFF_brands[0].capitalize()
        OFF_categories_to_strip = (OFF_product['categories']).split(',')
        self.OFF_categories = []
        for category in OFF_categories_to_strip:
            self.OFF_categories.append(category.strip().capitalize())
        OFF_stores_to_strip = (OFF_product['stores']).split(',')
        self.OFF_stores = []
        for store in OFF_stores_to_strip:
            self.OFF_stores.append(store.strip().capitalize())
        self.OFF_nutrition_grade = OFF_product['nutrition_grades'].lower()

    def _update_new_information(self, local_product):
        """ Manages information updating, except categories and stores.

        Compares product information from local database vs. from Open
        Food Facts API.
        Updates information when needed.

        """
        if local_product[0]['name'] != self.OFF_name:
            self.product_manager.update_name(self.OFF_name, self.OFF_code)
            print('"name" updated !', file=open('print_log.txt', 'a'))
        if local_product[0]['description'] != self.OFF_description:
            self.product_manager.update_description(self.OFF_description,
                                                    self.OFF_code)
            print('"description" updated !', file=open('print_log.txt', 'a'))
        if local_product[0]['brand'] != self.OFF_brand:
            self.product_manager.update_brand(self.OFF_brand, self.OFF_code)
            print('"brand" updated !', file=open('print_log.txt', 'a'))
        if local_product[0]['nutrition_grade'] != self.OFF_nutrition_grade:
            self.product_manager.update_nutrition_grade(
                self.OFF_nutrition_grade, self.OFF_code)
            print('"nutrition_grade" updated !',
                  file=open('print_log.txt', 'a'))

    def _update_categories_information(self, local_product):
        """ Manages categories information updating.

        - Retrieves categories information for local product.
        - Removes obsolete categories from local database.
        - Adds new categories from Open Food Facts into local database.

        """
        local_product_categories_id, local_product_categories_list = \
            self._get_local_product_categories_information()

        self._remove_obsolete_categories(local_product,
                                         local_product_categories_id,
                                         local_product_categories_list)
        self._add_new_categories(local_product_categories_list)

    def _get_local_product_categories_information(self):
        """ Manages retrieving of categories information.

        Returns categories id object & list of categories name for local
        product.

        """
        local_product_categories_id = self.product_category_manager.\
            select_based_on_product_id(self.OFF_code)

        local_product_categories_list = []

        for i in range(len(local_product_categories_id.all())):
            category_name = self.category_manager.select_based_on_id(
                local_product_categories_id[i]['category_id'])
            local_product_categories_list.append(category_name)

        return local_product_categories_id, local_product_categories_list

    def _remove_obsolete_categories(self, local_product,
                                    local_product_categories_id,
                                    local_product_categories_list):
        """ Manages obsolete categories removing.

        Removes categories from local database which don't appear
        anymore in Open Food Facts platform.

        """
        for i in range(len(local_product_categories_list)):
            if local_product_categories_list[i] not in self.OFF_categories:
                self.product_category_manager.delete(
                    self.OFF_code,
                    local_product_categories_id[i]['category_id'])

                product_category = self.product_category_manager.\
                    select_based_on_category_id(
                        local_product_categories_id[i]['category_id'])
                try:
                    category_id = product_category[0]['category_id']
                    # with open('print_log.txt', 'a') as f:
                    print(f'La catégorie {category_id} est associée à \
d\'autre(s) produit(s). On la conserve.',
                          file=open('print_log.txt', 'a'))
                except IndexError:
                    # with open('print_log.txt', 'a') as f:
                    print('La catégorie n\'est associée à aucun autre \
produit. On la supprime',
                          file=open('print_log.txt', 'a'))
                    self.category_manager.delete(
                        local_product_categories_list[i])

    def _add_new_categories(self, local_product_categories_list):
        """ Manages new categories addition.

        Adds new categories from Open Food Facts platform into local
        database.

        """
        for category in self.OFF_categories:
            if category not in local_product_categories_list:
                local_category = self.category_manager.\
                    select_based_on_name(category)
                try:
                    local_category_name = local_category[0]['name']
                    self.product_category_manager.insert(
                        local_category_name, self.OFF_name)
                except IndexError:
                    print('La catégorie n\'existe pas',
                          file=open('print_log.txt', 'a'))
                    self.category_manager.insert(category)
                    self.product_category_manager.insert(
                        category, self.OFF_name)

    def _update_stores_information(self, local_product):
        """ Manages stores information updating.

        - Retrieves stores information for local product.
        - Removes obsolete stores from local database.
        - Adds new stores from Open Food Facts into local database.

        """
        local_product_stores_id, local_product_stores_list = \
            self._get_local_product_stores_information()

        self._remove_obsolete_stores(local_product, local_product_stores_id,
                                     local_product_stores_list)
        self._add_new_stores(local_product_stores_list)

    def _get_local_product_stores_information(self):
        """ Manages retrieving of stores information.

        Returns stores id object & list of stores name for local
        product.

        """
        local_product_stores_id = self.product_store_manager.\
            select_based_on_product_id(self.OFF_code)

        local_product_stores_list = []

        for i in range(len(local_product_stores_id.all())):
            store_name = self.store_manager.select_based_on_id(
                local_product_stores_id[i]['store_id'])
            local_product_stores_list.append(store_name)

        return local_product_stores_id, local_product_stores_list

    def _remove_obsolete_stores(self, local_product, local_product_stores_id,
                                local_product_stores_list):
        """ Manages obsolete stores removing.

        Removes stores from local database which don't appear
        anymore in Open Food Facts platform.

        """
        for i in range(len(local_product_stores_list)):
            if local_product_stores_list[i] not in self.OFF_stores:
                self.product_store_manager.delete(
                    self.OFF_code, local_product_stores_id[i]['store_id'])

                product_store = self.product_store_manager.\
                    select_based_on_store_id(
                        local_product_stores_id[i]['store_id'])
                try:
                    store_id = product_store[0]['store_id']
                    print(f'Le magasin {store_id} est associé à \
d\'autre(s) produit(s). On le conserve.',
                          file=open('print_log.txt', 'a'))
                except IndexError:
                    print('Le magasin n\'est associé à aucun autre \
produit. On le supprime',
                          file=open('print_log.txt', 'a'))
                    self.store_manager.delete(local_product_stores_list[i])

    def _add_new_stores(self, local_product_stores_list):
        """ Manages new stores addition.

        Adds new stores from Open Food Facts platform into local
        database.

        """
        for store in self.OFF_stores:
            if store not in local_product_stores_list:
                local_store = self.store_manager.select_based_on_name(store)
                try:
                    local_store_name = local_store[0]['name']
                    self.product_store_manager.insert(local_store_name,
                                                      self.OFF_name)
                except IndexError:
                    print('Le magasin n\'existe pas',
                          file=open('print_log.txt', 'a'))
                    self.store_manager.insert(store)
                    self.product_store_manager.insert(store, self.OFF_name)

    def _save_update_date(self):
        """ Manages saving of database update date.

        Saves the date into an external 'last_update.txt' file.

        """
        update_date = str(time())

        with open('last_update.txt', "w") as f:
            f.write(update_date)
Ejemplo n.º 5
0
 def saving_categories(self, object_list):
     category_manager = CategoryManager()
     object_list = tuple(object_list)
     category_manager.save(object_list)
Ejemplo n.º 6
0
class ExpenseReport:
    """Generates the expense report.

    xfile:   (str) the CSV file containing the expenses.
    catfile: (str) the JSON file of the categories.
    """
    def __init__(self, xfile, catfile='cats.json'):
        self.filename = xfile

        self.catman = CategoryManager(catfile)

        self.expenses = list()
        self.figures = list()

    def generate_report(self):
        self._categorize_expenses()
        self._generate_graphs()
        self._generate_pdf()
        self._clean_graphs()

    def _categorize_expenses(self):
        logger.debug(f'Categorizing expenses from {self.filename}')

        Expense = namedtuple(
            'Expense',
            ['date', 'cat', 'subcat', 'expense', 'price']
        )

        date = ""
        with open(self.filename, 'r') as file:
            reader = DictReader(file)

            for row in reader:
                expense = row['Expense'].lower()
                price = float(row['Price'])

                # Reuse older date if not specified
                if row['Date'].strip() != "":
                    date = row['Date']

                query = self.catman.query(expense)

                if query is None:
                    self.catman.add(expense, price)
                    query = self.catman.query(expense)
                else:
                    self.catman.update(expense, price)

                self.expenses.append(Expense(
                    date=date,
                    cat=query['cat'],
                    subcat=query['subcat'],
                    expense=expense,
                    price=price
                ))

        logger.info(f'Successfully categorized expenses from {self.filename}')

    def _generate_graphs(self):
        logger.debug('Creating pie charts for overall and categorical expenses')

        # Organize data for pie charts
        overall = dict()
        category = dict()

        for x in self.expenses:
            # Add expense to overall categories
            if x.cat not in overall.keys():
                overall[x.cat] = 0

            overall[x.cat] += x.price

            # Add expense to subcategories
            if x.cat not in category.keys():
                category[x.cat] = dict()

            if x.subcat not in category[x.cat].keys():
                category[x.cat][x.subcat] = 0

            category[x.cat][x.subcat] += x.price

        overall = {'overall': overall}

        self._generate_pie_chart(overall)
        self._generate_pie_chart(category)

        logger.info('Successfully created all pie charts')

    def _generate_pie_chart(self, data):
        def pformat(pct, total):
            value = pct / 100.0 * total
            return f'${value:.2f}\n({pct:.0f}%)'

        if len(data) == 1:
            logger.debug('Creating single pie chart')

            # Clear previous plot
            plt.clf()
            plt.cla()

            fig, ax = plt.subplots(nrows=1, ncols=1, dpi=300, figsize=(6, 6))

            for title, cats in data.items():
                # Create pie chart
                ax.pie(
                    cats.values(),
                    labels=cats.keys(),
                    autopct=lambda x: pformat(x, sum(cats.values()))
                )
                ax.set(title=f'{title.title()} Expenses')

                # Keep track of created figures
                filename = f'{title.replace(" ", "_")}.png'
                plt.savefig(filename)
                self.figures.append(filename)
        else:
            logger.debug('Creating sheet of pie charts')

            # Clear previous plot
            plt.clf()
            plt.cla()
            fig, ax = plt.subplots(nrows=3, ncols=2, dpi=300, figsize=(6, 9))
            for indx, (cat, subcats) in enumerate(data.items()):
                row = (indx % 6) // 2
                col = indx % 2

                # Create pie chart
                ax[row, col].pie(
                    subcats.values(),
                    labels=subcats.keys(),
                    autopct=lambda x: pformat(x, sum(subcats.values()))
                )
                ax[row, col].set(title=f'{cat.title()} Expenses')

                # Make new plot if all six figures are filled
                if indx % 6 == 5:
                    logger.warning('All six pie charts are filled, creating a new sheet')

                    # Keep track of created figures
                    filename = f'category{indx // 6}.png'
                    plt.savefig(filename)
                    self.figures.append(filename)

                    # Clear previous plot
                    plt.clf()
                    plt.cla()

                    fig, ax = plt.subplots(nrows=2, ncols=3, dpi=300)
            else:
                logger.warning('Clearing empty pie charts')

                # Clear empty plots
                for i in range(indx + 1, (indx // 6 + 1) * 6):
                    row = (i % 6) // 2
                    col = i % 2

                    ax[row, col].axis('off')

                # Save the last figure
                filename = f'category{indx // 6}.png'
                plt.savefig(filename)
                self.figures.append(filename)

    def _generate_pdf(self):
        texgen = TexGenerator(self.filename[:-4] + '.tex')
        texgen.add_header()
        texgen.add_title(self.filename[:-4].replace('_', ' ').title())

        texgen.add_section('Expense Charts')

        for filename in self.figures:
            texgen.add_figure(filename)

        texgen.add_section('Expense Data')

        # Reorganize data with date first
        total = 0
        data = dict()
        for x in self.expenses:
            if x.date not in data.keys():
                data[x.date] = dict()

            if x.cat not in data[x.date].keys():
                data[x.date][x.cat] = dict()

            if x.subcat not in data[x.date][x.cat].keys():
                data[x.date][x.cat][x.subcat] = dict()

            if x.expense not in data[x.date][x.cat][x.subcat].keys():
                data[x.date][x.cat][x.subcat][x.expense] = 0

            data[x.date][x.cat][x.subcat][x.expense] += x.price
            total += x.price

        texgen.add_table(data, total)

        texgen.add_footer()

        texgen.write()
        texgen.compile()

    def _clean_graphs(self):
        logger.debug('Removing created graphs')

        for filename in self.figures:
            rmrf = subprocess.run(['rm', '-rf', filename])

        if rmrf.returncode == 0:
            logger.info('Successfully removed graphs')
        else:
            logger.error('Failed to remove graphs')

        self.figures = list()
Ejemplo n.º 7
0
class DatabaseFiller:
    """ Sets DatabaseFiller class.

    Consists of 3 private methods :
        - __init__()
        - _get_products()
        - _fill_db()

    """
    def __init__(self, database):
        """ DatabaseFiller constructor.

        For each category and for each nutrition grade, retrieves
        corresponding products from Open Food Facts API and adds them to
        local database.

        """
        # self.database = database
        # Creates instances of table manager classes
        self.product_manager = ProductManager(database)
        self.category_manager = CategoryManager(database)
        self.product_category_manager = ProductCategoryManager(database)
        self.store_manager = StoreManager(database)
        self.product_store_manager = ProductStoreManager(database)

        for nutrition_grade in nutrition_grades:
            for category in tag_categories:
                products = self._get_products(category, nutrition_grade)
                self._fill_db(products)

    def _get_products(self, category, nutrition_grade):
        """ Return a list of dictionnaries of products information.

        Connects to Open Food Facts API via get() method from requests
        library and sends requests using given criteria for categories
        and nutrition grades.
        json content from response object contains products information.

        """
        criteria = {
            'action': 'process',
            'json': 1,
            'countries': 'France',
            'page_size': 100,
            'page': 1,
            'tagtype_0': 'categories',
            'tag_contains_0': 'contains',
            'tag_0': category,
            'tagtype_1': 'nutrition_grades',
            'tag_contains_1': 'contains',
            'tag_1': nutrition_grade
        }

        response = get('https://fr.openfoodfacts.org/cgi/search.pl',
                       params=criteria)
        data = response.json()

        # 'products' is a list of dictionnaries
        products = data['products']
        return products

    def _fill_db(self, products):
        """ Manages database filling.

        Checks for each product whether required data is available or
        not.
        If so, product is added to local database.

        """
        for product in products:
            try:
                self.code = product['code']
                self.name = product['product_name'].strip().capitalize()
                self.description = product['generic_name'].capitalize()
                brands = (product['brands']).split(',')
                self.brand = brands[0].capitalize()
                self.url = product['url']
                categories_to_strip = (product['categories']).split(',')
                self.categories = []
                for category in categories_to_strip:
                    self.categories.append(category.strip().capitalize())
                stores_to_strip = (product['stores']).split(',')
                self.stores = []
                for store in stores_to_strip:
                    self.stores.append(store.strip().capitalize())
                self.nutrition_grade = product['nutrition_grades'].lower()

            except KeyError:
                print('Missing data', file=open('print_log.txt', 'a'))

            if all([
                    self.code, self.name, self.description, self.brand,
                    self.url, self.nutrition_grade, self.categories[0],
                    self.stores[0]
            ]):
                self.product_manager.insert(self.code, self.name,
                                            self.description, self.brand,
                                            self.url, self.nutrition_grade)
                for category in self.categories:
                    self.category_manager.insert(category)
                    self.product_category_manager.insert(category, self.name)
                for store in self.stores:
                    self.store_manager.insert(store)
                    self.product_store_manager.insert(store, self.name)