def fill_jinja_references(workbook, data):
    """Fill-in Jinga-style references, iterating over all
    worksheet pages to fill-in all occurrences of each reference 1 by 1.
    Args:
        workbook (Workbook): A Workbook object.
        data (dict): A dictionary of context data.
    """
    context = {snake_case(key): values for key, values in data.items()}
    sheets = workbook.worksheets
    for sheet in sheets:
        refs = get_jinja_references(sheet)
        for key, cells in refs.items():
            clean_key = snake_case(key)
            value = context.get(clean_key, 0)
            for cell in cells:
                if value:
                    sheet[cell] = str(value)
                else:
                    sheet[cell] = 'MISSING'
Exemple #2
0
def get_worksheet_headers(sheet):
    """Get the headres of a worksheet.
    Args:
        sheet (Worksheet): An openpyx; Excel file object.
    Returns:
        headers (list): A list of header strings.
    """
    headers = []
    for cell in sheet[1]:
        headers.append(snake_case(cell.value))
    return headers
Exemple #3
0
def get_data_block(sheet, coords, expand=None):
    """Get a data block.
    Args:
        sheet (Sheet): The worksheet containing the data block.
        coords (str): The inclusive coordinates of the data block.
        expand (str): Optionally expand the range of values.
    Returns
        (dict): A dictionary of the data in the data block.
    """
    data = {}
    values = sheet.range(coords).options(expand=expand).value
    for item in values:
        key = snake_case(item[0])
        value = item[1]
        data[key] = value
    return data
Exemple #4
0
def import_worksheet(filename, sheetname, range_start='A1'):
    """Read the data from a given worksheet using xlwings.
    Args:
        filename (str): The name of the Excel file to read.
        range_start (str): Optional starting cell.
    Returns:
        list(dict): A list of dictionaries.
    """
    app = xlwings.App(visible=False)
    book = xlwings.Book(filename)
    sheet = book.sheets(sheetname)
    excel_data = sheet.range(range_start).expand('table').value
    keys = [snake_case(key) for key in excel_data[0]]
    data = [dict(zip(keys, values)) for values in excel_data[1:]]
    book.close()
    app.quit()
    return data
Exemple #5
0
def read_worksheet(path, filename='Upload'):
    """Read the imported data, iterating over the rows and
    getting value from each cell in row.
    Args:
        path (str or InMemoryFile): An Excel workbook to read.
        filename (str): The name of the worksheet to upload.
    Returns:
        (DataFrame): A Pandas DataFrame of the results.
    """
    # FIXME:
    # try:
    #     workbook = openpyxl.load_workbook(path, data_only=True)
    #     sheet = workbook.get_sheet_by_name(filename)
    #     headers = get_worksheet_headers(sheet)
    #     return pd.DataFrame(get_worksheet_data(sheet, headers))
    # except:
    print('Path:', path)
    data = pd.read_csv(path)
    data.columns = [snake_case(x) for x in data.columns]
    return data
Exemple #6
0
def import_worksheet_data(model_type):
    """A function called from Excel to import data by ID
    from Firestore into the Excel workbook.
    Args:
        model_type (str): The data model at hand.
    """

    # Initialize the workbook.
    book = xlwings.Book.caller()
    worksheet = book.sheets.active
    config_sheet = book.sheets['cannlytics.conf']
    config = get_data_block(config_sheet, 'A1', expand='table')
    show_status_message(
        worksheet,
        coords=config['status_cell'],
        message='Importing %s data...' % model_type,
        background=config['success_color'],
    )

    # Read the IDs.
    id_cell = increment_row(config['table_cell'])
    ids = worksheet.range(id_cell).options(expand='down', ndim=1).value

    # Get your Cannlytics API key from your .env file, location specified
    # by env_path on the cannlytics.config sheet.
    load_dotenv(config['env_path'])
    api_key = os.getenv('CANNLYTICS_API_KEY')

    # Get the worksheet columns.
    columns = worksheet.range(config['table_cell']).options(expand='right',
                                                            ndim=1).value
    columns = [snake_case(x) for x in columns]

    # Get data using model type and ID through the API.
    base = config['api_url']
    org_id = worksheet.range(config['org_id_cell']).value
    headers = {
        'Authorization': 'Bearer %s' % api_key,
        'Content-type': 'application/json',
    }
    if len(ids) == 1:
        url = f'{base}/{model_type}/{ids[0]}?organization_id={org_id}'
    else:
        url = f'{base}/{model_type}?organization_id={org_id}&items={str(ids)}'
    response = requests.get(url, headers=headers)
    if response.status_code != 200:
        show_status_message(worksheet,
                            coords=config['status_cell'],
                            message='Error importing data.',
                            background=config['error_color'])
        return

    # Format the values.
    items = []
    data = response.json()['data']
    if not data:
        show_status_message(worksheet,
                            coords=config['status_cell'],
                            message='No data found.',
                            background=config['error_color'])
        return
    try:
        for item in data:
            values = []
            for column in columns:
                values.append(item.get(column))
            items.append(values)
    except AttributeError:
        values = []
        for column in columns:
            values.append(data.get(column))
        items = [values]

    # Insert all rows at the same time.
    worksheet.range(id_cell).value = items

    # Show success status message.
    show_status_message(
        worksheet,
        coords=config['status_cell'],
        message='Imported %i %s.' % (len(ids), model_type),
    )
def generate_coas(
        import_files,
        output_pages,
        coa_template='./coa_template.xlsm',
        # render_file='./CoAs/coa_render.xlsm',
        limits={}):
    """Generate certificates of analysis.
    Args:
        import_files (list): A list of files to import.
        output_pages (list): A list of pages to include in the PDF.
        coa_template (str): The path of the CoA Template.
        limits (dict): A dictionary of limits and LOQ for analytes.
    """

    # Create CoA folder if one does not exist.
    Path('CoAs').mkdir(parents=True, exist_ok=True)
    dir_path = os.path.dirname(os.path.realpath(__file__))

    # Create a copy of the template.
    abs_coa_template = os.path.join(dir_path, coa_template)
    coa_template_copy = abs_coa_template.replace('.xlsm', '_copy.xlsm')
    copyfile(abs_coa_template, coa_template_copy)

    # Iterate over all import files.
    for import_file in import_files:

        # Get all sample, results, client, etc. data.
        # abs_import_file = os.path.join(dir_path, import_file)
        all_data = pd.read_excel(import_file)

        # Get all the masses.
        masses = {}
        for _, row in all_data.iterrows():
            key = snake_case(row['assay'])
            masses[key] = row['test_mass']

        # Aggregate all data for a sample
        data = all_data.groupby('sample_id', as_index=False).first()

        # Fill in sample details.
        for _, row in data.iterrows():

            # Get sample data as a dictionary.
            sample_data = {**row.to_dict(), **limits}
            sample_id = sample_data['sample_id']
            if not sample_id:  # FIXME: Skip nan
                continue

            # Calculate terpene and cannabinoid results.
            sample_data = calculate_results(sample_data,
                                            analysis='terpenes',
                                            mass=masses['terpenes'],
                                            dilution_factor=40)
            sample_data = calculate_results(sample_data,
                                            analysis='cannabinoids',
                                            mass=masses['potency'],
                                            dilution_factor=40 * 50)

            # Iterate over worksheet pages to fill-in
            # all occurrences of each reference 1 by 1.
            template_workbook = openpyxl.load_workbook(coa_template_copy,
                                                       keep_vba=True)
            fill_jinja_references(template_workbook, sample_data)

            # FIXME: Get output pages dynamically
            try:
                ws_index_list = get_worksheet_indexes(template_workbook,
                                                      output_pages)
            except:
                if len(output_pages) == 3:
                    ws_index_list = [3, 4, 5]
                else:
                    ws_index_list = [3, 4, 5, 6]

            # Save the rendered template, temporarily.
            abs_render_file = os.path.join(dir_path, f'CoAs/{sample_id}.xlsm')
            template_workbook.save(abs_render_file)

            # Future: Insert signatures
            # Future: Insert QR Code
            # Future: Touch up the CoA.
            # ws.oddHeader.left.text = "Page &[Page] of &N"
            # Mark failures as red
            # a1.font = Font(color="FF0000", italic=True) # the change only affects A1

            # Create a PDF.
            output_file = f'CoAs/{sample_id}.pdf'
            # FIXME:
            excel = create_coa_pdfs(abs_render_file, ws_index_list,
                                    output_file)

            # Future: Upload the PDF.
            # Future: Create download link and short link for the CoA.
            # Future: Upload the CoA data.
            # - See https://cloud.google.com/functions/docs/writing/http#uploading_files_via_cloud_storage

    # Remove temporary files.
    # os.remove(abs_render_file)
    os.remove(coa_template_copy)

    # Ensure Excel is visible.
    excel.ScreenUpdating = True
    excel.DisplayAlerts = True
    excel.EnableEvents = True
def test_snake_case():
    """Test string to snake-case."""
    key = snake_case('% Dev. from the mean')
    assert key == 'percent_dev_from_the_mean'