def _bulk_create_part_inventories(api: Api, df: pd.DataFrame, parts_dict: dict) -> dict: """ Bulk create inventory items for every unique serial and part number in CSV. Args: api (Api): API instance to send authenticated requests df (pd.DataFrame): Dataframe of batch run creation CSV parts_dict (dict): Mapping from part number to part id Returns: dict: Mapping of part/serial number tuple to newly created inventory id """ create_mutations = [] for part_info in df.groupby(['Part number', 'Serial number']).indices: mutation_input = {'serialNumber': part_info[1], 'quantity': 1, 'partId': parts_dict[part_info[0]]} create_mutations.append( {'query': mutations.CREATE_PART_INVENTORY, 'variables': {'input': mutation_input}}) inventories = api.send_api_request(create_mutations) inventory_dict = {} for inventory in inventories: item = inventory['data']['createPartInventory']['partInventory'] inventory_dict[(item['part']['partNumber'], item['serialNumber'])] = item return inventory_dict
def import_values(api: Api, input_file: str) -> None: df = get_parts_df(input_file) logging.info('Starting part importer.') to_upload = get_upload_dict(df) to_upload = create_upload_items(df, to_upload) # Get API token access_token = api.get_access_token() cache = {} # Get pre existing uoms to_upload['uoms'], cache = get_existing_items( auth_token=access_token, values=to_upload['uoms'], unique_field='type', query_type='GET_UNITS_OF_MEASUREMENTS', cache_type='unit_of_measurement', cache=cache) # Get pre existing locations to_upload['locations'], cache = get_existing_items( auth_token=access_token, values=to_upload['locations'], unique_field='name', query_type='GET_LOCATIONS', cache_type='location', cache=cache) # Upload parts, units of measurment and locations resp = create_bulk_upload_request( access_token, CREATE_UNITS_OF_MEASUREMENT=to_upload['uoms'], CREATE_PART=to_upload['parts'], CREATE_LOCATION=to_upload['locations']) # Fill cache with ids from newely created objects cache = update_cache(resp, cache) to_upload = fill_from_cache(to_upload=to_upload, cache=cache, to_fill=['parts_inventories']) # Upload part inventories and part lots resp = create_bulk_upload_request( access_token, CREATE_PART_INVENTORY=to_upload['parts_inventories'],) logging.info('Importing finished!')
def import_bom(api: Api, input_file: str) -> None: """ Parse SolidWorks BOM export into pandas DataFrame. Upload each row from the excel file into ION as an MBOM item. If the part being referenced by the MBOM item does not exist, create that as well. Args: input_file (str): Path to SolidWorks BOM to be imported Returns: bool: Returns true if the BOM was successfully imported """ # Read SolidWorks Level as string to correctly parse hierarchy. df = pd.read_excel(input_file, dtype={'Level': str, 'Part Number': str}) # Use level as index df.set_index('Level', inplace=True) # Get top level part from file name top_level_part_number = args.input_file.split('/')[-1].split('.')[0] # TODO: Replace access token fetching with just using the Api object # and abstract access tokens and requests from business logic # Get API token access_token = api.get_access_token() part_dict, part_numbers = _get_parts_info(access_token, df, top_level_part_number) _create_mbom_items(access_token, df, part_dict, part_numbers) return True
def _get_locations(api: Api, df: pd.DataFrame) -> dict: """ Get ids for all location names in the excel sheet. Args: api (Api): API instance to send authenticated requests df (pd.DataFrame): Dataframe of excel file passed in arguments Returns: dict: Mapping from location name to location id """ query_info = { 'query': mutations.GET_LOCATIONS, 'variables': { 'filters': { 'name': { 'in': df['Location'].unique().tolist() } } } } query_data = api.send_api_request(query_info) return { edge['node']['name']: edge['node']['id'] for edge in query_data['data']['locations']['edges'] }
def _get_parts(api: Api, df: pd.DataFrame) -> dict: """ Get ids for all part numbers in the excel sheet. Args: api (Api): API instance to send authenticated requests df (pd.DataFrame): Dataframe of excel file passed in arguments Returns: dict: Mapping from part number to part id """ query_info = { 'query': mutations.GET_PARTS, 'variables': { 'filters': { 'partNumber': { 'in': df['Part Number'].unique().tolist() }, 'isLatestRevision': { 'eq': True } } } } query_data = api.send_api_request(query_info) return { edge['node']['partNumber']: edge['node']['id'] for edge in query_data['data']['parts']['edges'] }
def _create_mbom(api: Api, mbom_map: dict, parts: dict) -> bool: """ Batch create MBOM items for every part in the import excel. Args: api (Api): API instance to send authenticated requests mbom_map (dict): Mapping of part number to parent part and quantity parts (dict): Mapping from part number to part id Returns: bool: True if part import was successful. """ create_mutations = [] for part_number, mbom_item in mbom_map.items(): if mbom_item['parent'] in parts and part_number in parts: mutation_input = { 'partId': parts[part_number], 'parentId': parts[mbom_item['parent']], 'quantity': mbom_item['quantity'] } create_mutations.append({ 'query': mutations.CREATE_MBOM_ITEM, 'variables': { 'input': mutation_input } }) mboms = api.send_api_request(create_mutations) for idx, mbom_item in enumerate(mboms): if 'errors' in mbom_item and len(mbom_item['errors']) > 0: logging.warning(mbom_item['errors'][0]['message']) return True
def _bulk_create_part_inventories(api: Api, df: pd.DataFrame, parts: dict, locations: dict) -> bool: """ Bulk create inventory items for every row in excel. Validates that the part number the inventory object is referencing exists. Args: api (Api): API instance to send authenticated requests df (pd.DataFrame): Dataframe of excel file passed in arguments parts (dict): Mapping from part number to part id Returns: bool: True if inventory import was successful. """ create_mutations = [] for _, row in df.iterrows(): if row['Part Number'] not in parts: logging.warning('Cannot create inventory because part ' f'{row["Part Number"]} does not exist.') continue quantity = row['Quantity'] if row['Serial Number'] is None else 1 location = None if row['Location'] in locations: location = locations[row['Location']] mutation_input = { 'serialNumber': row['Serial Number'], 'quantity': quantity, 'partId': parts[row['Part Number']], 'lotNumber': row['Lot Number'], 'locationId': location } create_mutations.append({ 'query': mutations.CREATE_PART_INVENTORY, 'variables': { 'input': mutation_input } }) inventories = api.send_api_request(create_mutations) for idx, inventory in enumerate(inventories): if 'errors' in inventory and len(inventory['errors']) > 0: logging.warning(inventory['errors'][0]['message']) return True
def _get_procedures(api: Api, df: pd.DataFrame) -> dict: """ Get procedure titles for all procedures in the CSV. Args: api (Api): API instance to send authenticated requests df (pd.DataFrame): Dataframe of batch run creation CSV Returns: dict: Mapping from procedure id to procedure title """ query_info = { 'query': mutations.GET_PROCEDURES, 'variables': { 'filters': {'id': {'in': df['Procedure (ID)'].unique().tolist()}} } } query_data = api.send_api_request(query_info) return {edge['node']['id']: edge['node']['title'] for edge in query_data['data']['procedures']['edges']}
def _get_parts(api: Api, df: pd.DataFrame) -> dict: """ Get ids for all part numbers in the CSV. Args: api (Api): API instance to send authenticated requests df (pd.DataFrame): Dataframe of batch run creation CSV Returns: dict: Mapping from part number to part id """ query_info = { 'query': mutations.GET_PARTS, 'variables': { 'filters': {'partNumber': {'in': df['Part number'].unique().tolist()}} } } query_data = api.send_api_request(query_info) return {edge['node']['partNumber']: edge['node']['id'] for edge in query_data['data']['parts']['edges']}
def _batch_create_runs(api: Api, df: pd.DataFrame, procedures: dict, inventory_dict: dict, parts: dict) -> bool: """ Batch create runs for every row in the import CSV. Args: api (Api): API instance to send authenticated requests df (pd.DataFrame): Dataframe of batch run creation CSV procedures (dict): Mapping from procedure id to procedure title inventory (dict): Mapping of part/serial number tuple to inventory id parts_dict (dict): Mapping from part number to part id Returns: bool: True if runs were successfully created. """ create_mutations = [] for _, row in df.iterrows(): procedure_id = row['Procedure (ID)'] inventory = inventory_dict.get((row["Part number"], row["Serial number"]), {}) title = row['Run title (leave blank for default format*)'] if not isinstance(title, str): procedure = procedures[procedure_id] title = f'{row["Part number"]} - {row["Serial number"]} - {procedure}' mutation_input = {'title': title, 'procedureId': procedure_id, 'partInventoryId': inventory.get('id', None), 'partId': parts[row['Part number']]} create_mutations.append( {'query': mutations.CREATE_RUN, 'variables': {'input': mutation_input}}) # CREATE ABOM TRACE FOR INVENTORY if inventory: create_mutations.append( {'query': mutations.CREATE_ABOM_FOR_PART_INVENTORY, 'variables': {'id': inventory['id'], 'etag': inventory['_etag']}}) runs = api.send_api_request(create_mutations) for run in runs: if 'errors' in run and len(run['errors']) > 0: logging.warning(run['errors'][0]['message']) return True
"""Verify connection and authentication with the target API.""" import argparse from getpass import getpass import logging import pandas as pd import sys import os sys.path.append(os.getcwd()) from importers import Api, API_URL logging.basicConfig(level=logging.INFO, format='[%(asctime)s] - [%(levelname)s] - %(message)s') if __name__ == "__main__": parser = argparse.ArgumentParser( description='Verify connection and authentication with the ion API.') parser.add_argument('--client_id', type=str, help='Your API client ID') args = parser.parse_args() client_secret = getpass('Client secret: ') if not args.client_id or not client_secret: raise argparse.ArgumentError('Must input client ID and client secret.') try: api = Api(client_id=args.client_id, client_secret=client_secret) print('Successful connection!') print(f'API: {API_URL}') print(f'Audience: {api.audience}') print(f'Client ID: {api.client_id}') except KeyError: raise ConnectionError('Not able to connect to API.')
def _bulk_create_parts(api: Api, df: pd.DataFrame, parts: dict) -> bool: """ Batch create parts and MBOM relations for every row in the import excel file. For each row representing a part, there is also a depth and quantity column which define its MBOM relations. Depth is an int value defining a part as a child to another part which is the first instance of the current row's depth - 1. Example Rows: Depth Part Number 1 fr-1 2 fr-2 3 fr-3 3 fr-4 2 fr-5 This defines fr-1 as the top level assembly with children fr-2 and fr-5. Also fr-3 and fr-4 represent the MBOM for fr-2. Args: api (Api): API instance to send authenticated requests df (pd.DataFrame): Dataframe of excel file passed in arguments parts (dict): Mapping from part number to part id Returns: bool: True if part import was successful. """ mbom_map = {} create_mutations = [] depth = 0 parent_part_queue = [] parts_dict = {} for idx, row in df.iterrows(): if row['Part Number'] in parts: logging.warning( f'Cannot create part {row["Part Number"]} because it already exists.' ) if row['Depth'] > depth: parent_part_queue.append(row["Part Number"]) elif row['Depth'] < depth: parent_part_queue = parent_part_queue[:row['Depth'] - depth - 1] parent_part_queue.append(row["Part Number"]) else: parent_part_queue[-1] = row["Part Number"] if len(parent_part_queue) > 1: mbom_map[row['Part Number']] = { 'parent': parent_part_queue[-2], 'quantity': row['Quantity'] } depth = row['Depth'] mutation_input = { 'partNumber': row['Part Number'], 'description': row['Description'], 'trackingType': row['Tracking Level'].upper() } if row.get('Revision', None) is not None: mutation_input['revision'] = row.get('Revision', None) create_mutations.append({ 'query': mutations.CREATE_PART, 'variables': { 'input': mutation_input } }) parts = api.send_api_request(create_mutations) for idx, part in enumerate(parts): if 'errors' in part and len(part['errors']) > 0: logging.warning(part['errors'][0]['message']) else: p = part['data']['createPart']['part'] parts_dict[p['partNumber']] = p['id'] return _create_mbom(api, mbom_map, parts_dict)