06-09-2021 10:54 AM
on several nexus switches, I run the command: Show cdp neighbors detail | json-pretty
I want to iterate through it to grab "device_id", "v4addr", "platform_id", "intf_id", and "port_id" but I'm struggling on the way its been formatted.
Why is it {"table": {"row": [{ "key":"value", "key":"value", "key":"value", "capability": ["router", "switch"], "key":"value"}] }} and how do I iterate through a table, row, and the extra capability list/dict thing?
I tried:
import json objectJson = { "TABLE_cdp_neighbor_detail_info": { "ROW_cdp_neighbor_detail_info": [ { "ifindex": "824352435", "device_id": "mynexuscore-2", "sysname": "dc-core2", "numaddr": "1", "v4addr": "10.128.1.5", "platform_id": "N9K-C9372PX", "capability": [ "router", "switch", "Supports-STP-Dispute" ], "intf_id": "mgmt0", "port_id": "mgmt0", "ttl": "156", "version": "Cisco Nexus Operating System (NX-OS) Software, Version 7.0(3)I4(8a)", "version_no": "v2", "duplexmode": "full", "mtu": "1500", "syslocation": "snmplocation", "num_mgmtaddr": "1", "v4mgmtaddr": "10.128.1.5" }, { "ifindex": "436207616", "device_id": "tracer-01", "numaddr": "2", "v4addr": [ "192.168.254.50", "192.168.5.50" ], "platform_id": "N3000", "capability": "router", "intf_id": "Ethernet1/1", "port_id": "Te3/0/1", "ttl": "153", "version": "6.5.2.5", "version_no": "v2", "nativevlan": "250", "mtu": "0", "num_mgmtaddr": "0" }, { "ifindex": "4362246246", "device_id": "mynexuscore-2Standby", "numaddr": "2", "v4addr": "10.128.1.15", "v6addr": "fe80::5a97:bdff:bbbb:aaaa", "platform_id": "AIR-CT5520-K9", "capability": "host", "intf_id": "Ethernet1/3", "port_id": "TenGigabitEthernet0/0/1", "ttl": "125", "version": "Manufacturer's Name: Cisco Systems Inc. Product Name: Cisco Controller Product Version: 8.5.171.0 RTOS Version: 8.5.171.0 Bootloader Version: 8.1.102.0 Build Type: DATA + WPS", "version_no": "v2", "duplexmode": "full", "mtu": "0", "num_mgmtaddr": "0" }, { "ifindex": "436209152", "device_id": "WLC-1", "numaddr": "2", "v4addr": "10.128.1.16", "v6addr": "fe80::72e4:22ff:bbbb:aaaa", "platform_id": "AIR-CT5520-K9", "capability": "host", "intf_id": "Ethernet1/4", "port_id": "TenGigabitEthernet0/0/1", "ttl": "158", "version": "Manufacturer's Name: Cisco Systems Inc. Product Name: Cisco Controller Product Version:8.5.171.0 RTOS Version: 8.5.171.0 Bootloader Version: 8.1.102.0 Build Type: DATA + WPS", "version_no": "v2", "duplexmode": "full", "mtu": "0", "num_mgmtaddr": "0" }, { "ifindex": "436205555", "device_id": "MDF-1", "vtpname": "thisguy", "numaddr": "1", "v4addr": "10.128.1.18", "platform_id": "cisco WS-C2960X-48FPD-L", "capability": [ "switch", "IGMP_cnd_filtering" ], "intf_id": "Ethernet1/5", "port_id": "TenGigabitEthernet1/0/1", "ttl": "165", "version": "Cisco IOS Software, C2960X Software (C2960X-UNIVERSALK9-M), Version 15.2(2)E3, RELEASE SOFTWARE (fc3)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2015 by Cisco Systems, Inc.\nCompiled Wed 26-Aug-15 07:12 by prod_rel_team", "version_no": "v2", "nativevlan": "1", "duplexmode": "full", "mtu": "0", "num_mgmtaddr": "1", "v4mgmtaddr": "10.128.1.13" } ] } } dictionary = json.dumps(objectJson) print("before loop") for key in dictionary: if key == "device_id": print (key) Print("key loop") print("after loop)")
Note: this is not the original attempt. I've built some full stack web apps before using flask and javascript but this has been more frustrating to navigate through than any of that.
I cant even get the device_id to print for each device.
Solved! Go to Solution.
06-09-2021 02:40 PM - edited 06-10-2021 06:11 AM
HI @Esmogyi
Your outermost object is a dictionary and then within those top level key/value pairs you have strings and lists of dictionaries.
Wrapping your head around these complex data structures made my head hurt but once you get it its amazing what you can do.
Not sure if this will help:
https://gratuitous-arp.net/decomposing-complex-json-data-structures/
One tip is to start at the top level and sort of peel the layers back. I know from the first character "{" that I have a dictionary
You can break out key/value pairs but a good shortcut with some of these complex structures is to look at the keys.
>>> print(objectJson.keys()) dict_keys(['TABLE_cdp_neighbor_detail_info']) >>>
This is a dictionary with on key so I know the good stuff is at least one level deep.
Key/value break out if you have more than one key can be helpful. Notice I want the key and then what type of object it has for a value.
>>> for key, value in objectJson.items(): ... print(f"key: {key} \t val: {type(value)}") ... key: TABLE_cdp_neighbor_detail_info val: <class 'dict'> >>>
going to the next level:
>>> for key, value in objectJson['TABLE_cdp_neighbor_detail_info'].items(): ... print(f"key: {key} \t val: {type(value)}") ... key: ROW_cdp_neighbor_detail_info val: <class 'list'>
OK..now I have one more key and then I probably have the stuff I care about.
I now know that this structure is a list.
objectJson['TABLE_cdp_neighbor_detail_info']['ROW_cdp_neighbor_detail_info']
Lets see how many rows we have:
print(len(objectJson['TABLE_cdp_neighbor_detail_info']['ROW_cdp_neighbor_detail_info'])) 5
Sometimes its easier to put that long thing that is a list into a new variable as you can see below I put into "results_list" and then I iterate over results list.
>>> results_list = objectJson['TABLE_cdp_neighbor_detail_info']['ROW_cdp_neighbor_detail_info']
>>> for row in results_list: ... print(row) ... print() ... {'ifindex': '824352435', 'device_id': 'mynexuscore-2', 'sysname': 'dc-core2', 'numaddr': '1', 'v4addr': '10.128.1.5', 'platform_id': 'N9K-C9372PX', 'capability': ['router', 'switch', 'Supports-STP-Dispute'], 'intf_id': 'mgmt0', 'port_id': 'mgmt0', 'ttl': '156', 'version': 'Cisco Nexus Operating System (NX-OS) Software, Version 7.0(3)I4(8a)', 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '1500', 'syslocation': 'snmplocation', 'num_mgmtaddr': '1', 'v4mgmtaddr': '10.128.1.5'} {'ifindex': '436207616', 'device_id': 'tracer-01', 'numaddr': '2', 'v4addr': ['192.168.254.50', '192.168.5.50'], 'platform_id': 'N3000', 'capability': 'router', 'intf_id': 'Ethernet1/1', 'port_id': 'Te3/0/1', 'ttl': '153', 'version': '6.5.2.5', 'version_no': 'v2', 'nativevlan': '250', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '4362246246', 'device_id': 'mynexuscore-2Standby', 'numaddr': '2', 'v4addr': '10.128.1.15', 'v6addr': 'fe80::5a97:bdff:bbbb:aaaa', 'platform_id': 'AIR-CT5520-K9', 'capability': 'host', 'intf_id': 'Ethernet1/3', 'port_id': 'TenGigabitEthernet0/0/1', 'ttl': '125', 'version': "Manufacturer's Name: Cisco Systems Inc. Product Name: Cisco Controller Product Version: 8.5.171.0 RTOS Version: 8.5.171.0 Bootloader Version: 8.1.102.0 Build Type: DATA + WPS", 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '436209152', 'device_id': 'WLC-1', 'numaddr': '2', 'v4addr': '10.128.1.16', 'v6addr': 'fe80::72e4:22ff:bbbb:aaaa', 'platform_id': 'AIR-CT5520-K9', 'capability': 'host', 'intf_id': 'Ethernet1/4', 'port_id': 'TenGigabitEthernet0/0/1', 'ttl': '158', 'version': "Manufacturer's Name: Cisco Systems Inc. Product Name: Cisco Controller Product Version:8.5.171.0 RTOS Version: 8.5.171.0 Bootloader Version: 8.1.102.0 Build Type: DATA + WPS", 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '436205555', 'device_id': 'MDF-1', 'vtpname': 'thisguy', 'numaddr': '1', 'v4addr': '10.128.1.18', 'platform_id': 'cisco WS-C2960X-48FPD-L', 'capability': ['switch', 'IGMP_cnd_filtering'], 'intf_id': 'Ethernet1/5', 'port_id': 'TenGigabitEthernet1/0/1', 'ttl': '165', 'version': 'Cisco IOS Software, C2960X Software (C2960X-UNIVERSALK9-M), Version 15.2(2)E3, RELEASE SOFTWARE (fc3)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2015 by Cisco Systems, Inc.\nCompiled Wed 26-Aug-15 07:12 by prod_rel_team', 'version_no': 'v2', 'nativevlan': '1', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '1', 'v4mgmtaddr': '10.128.1.13'} >>>
Now you can see that each row in the list is a dictionary. Well we know how to process that now.
Lest look at the keys we have available to us
>>> for row in results_list: ... print(row.keys()) ... dict_keys(['ifindex', 'device_id', 'sysname', 'numaddr', 'v4addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'duplexmode', 'mtu', 'syslocation', 'num_mgmtaddr', 'v4mgmtaddr']) dict_keys(['ifindex', 'device_id', 'numaddr', 'v4addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'nativevlan', 'mtu', 'num_mgmtaddr']) dict_keys(['ifindex', 'device_id', 'numaddr', 'v4addr', 'v6addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'duplexmode', 'mtu', 'num_mgmtaddr']) dict_keys(['ifindex', 'device_id', 'numaddr', 'v4addr', 'v6addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'duplexmode', 'mtu', 'num_mgmtaddr']) dict_keys(['ifindex', 'device_id', 'vtpname', 'numaddr', 'v4addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'nativevlan', 'duplexmode', 'mtu', 'num_mgmtaddr', 'v4mgmtaddr']) >>>
OK lets say I want platform and v4IP:
>>> for row in results_list: ... print(f"Platform: {row['platform_id']} IP: {row['v4addr']}") ... Platform: N9K-C9372PX IP: 10.128.1.5 Platform: N3000 IP: ['192.168.254.50', '192.168.5.50'] Platform: AIR-CT5520-K9 IP: 10.128.1.15 Platform: AIR-CT5520-K9 IP: 10.128.1.16 Platform: cisco WS-C2960X-48FPD-L IP: 10.128.1.18 >>>
With all of this you can now see that some of the values are lists (devices with multiple IPs) so one more step to iterate over those if you need to.
Spend some time working through these exercises yourself. Knowing how to deal with these complex structures will serve you well!
errors:
this is like the first example.
06-09-2021 12:15 PM - edited 06-09-2021 12:17 PM
Hi @Esmogyi
You don't need the json.dump().
You simply use the dictionary object.
objectJson = {
"TABLE_cdp_neighbor_detail_info": { "ROW_cdp_neighbor_detail_info": [ {...} ] }
} for neighbor in objectJson['TABLE_cdp_neighbor_detail_info']['ROW_cdp_neighbor_detail_info']: print(neighbor) {'ifindex': '824352435', 'device_id': 'mynexuscore-2', 'sysname': 'dc-core2', 'numaddr': '1', 'v4addr': '10.128.1.5', 'platform_id': 'N9K-C9372PX', 'capability': ['router', 'switch', 'Supports-STP-Dispute'], 'intf_id': 'mgmt0', 'port_id': 'mgmt0', 'ttl': '156', 'version': 'Cisco Nexus Operating System (NX-OS) Software, Version 7.0(3)I4(8a)', 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '1500', 'syslocation': 'snmplocation', 'num_mgmtaddr': '1', 'v4mgmtaddr': '10.128.1.5'} {'ifindex': '436207616', 'device_id': 'tracer-01', 'numaddr': '2', 'v4addr': ['192.168.254.50', '192.168.5.50'], 'platform_id': 'N3000', 'capability': 'router', 'intf_id': 'Ethernet1/1', 'port_id': 'Te3/0/1', 'ttl': '153', 'version': '6.5.2.5', 'version_no': 'v2', 'nativevlan': '250', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '4362246246', 'device_id': 'mynexuscore-2Standby', 'numaddr': '2', 'v4addr': '10.128.1.15', 'v6addr': 'fe80::5a97:bdff:bbbb:aaaa', 'platform_id': 'AIR-CT5520-K9', 'capability': 'host', 'intf_id': 'Ethernet1/3', 'port_id': 'TenGigabitEthernet0/0/1', 'ttl': '125', 'version': "Manufacturer's Name: Cisco Systems Inc. Product Name: Cisco Controller Product Version: 8.5.171.0 RTOS Version: 8.5.171.0 Bootloader Version: 8.1.102.0 Build Type: DATA + WPS", 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '436209152', 'device_id': 'WLC-1', 'numaddr': '2', 'v4addr': '10.128.1.16', 'v6addr': 'fe80::72e4:22ff:bbbb:aaaa', 'platform_id': 'AIR-CT5520-K9', 'capability': 'host', 'intf_id': 'Ethernet1/4', 'port_id': 'TenGigabitEthernet0/0/1', 'ttl': '158', 'version': "Manufacturer's Name: Cisco Systems Inc. Product Name: Cisco Controller Product Version:8.5.171.0 RTOS Version: 8.5.171.0 Bootloader Version: 8.1.102.0 Build Type: DATA + WPS", 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '436205555', 'device_id': 'MDF-1', 'vtpname': 'thisguy', 'numaddr': '1', 'v4addr': '10.128.1.18', 'platform_id': 'cisco WS-C2960X-48FPD-L', 'capability': ['switch', 'IGMP_cnd_filtering'], 'intf_id': 'Ethernet1/5', 'port_id': 'TenGigabitEthernet1/0/1', 'ttl': '165', 'version': 'Cisco IOS Software, C2960X Software (C2960X-UNIVERSALK9-M), Version 15.2(2)E3, RELEASE SOFTWARE (fc3)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2015 by Cisco Systems, Inc.\nCompiled Wed 26-Aug-15 07:12 by prod_rel_team', 'version_no': 'v2', 'nativevlan': '1', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '1', 'v4mgmtaddr': '10.128.1.13'}
I printed the full neighbor details, but you can access/print any other key inside the entry.
Stay safe,
Sergiu
06-09-2021 01:45 PM - edited 06-09-2021 01:47 PM
playing around this code and trying to use objectJson.items()["TABLE.."]["ROW.."]: , flopping around parameters, i've gotten several different errors but not a single value of a device_id
for key, value in objectJson['TABLE_cdp_neighbor_detail_info']['ROW_cdp_neighbor_detail_info']: if key == "device_id": print(str(value))
errors:
06-09-2021 02:40 PM - edited 06-10-2021 06:11 AM
HI @Esmogyi
Your outermost object is a dictionary and then within those top level key/value pairs you have strings and lists of dictionaries.
Wrapping your head around these complex data structures made my head hurt but once you get it its amazing what you can do.
Not sure if this will help:
https://gratuitous-arp.net/decomposing-complex-json-data-structures/
One tip is to start at the top level and sort of peel the layers back. I know from the first character "{" that I have a dictionary
You can break out key/value pairs but a good shortcut with some of these complex structures is to look at the keys.
>>> print(objectJson.keys()) dict_keys(['TABLE_cdp_neighbor_detail_info']) >>>
This is a dictionary with on key so I know the good stuff is at least one level deep.
Key/value break out if you have more than one key can be helpful. Notice I want the key and then what type of object it has for a value.
>>> for key, value in objectJson.items(): ... print(f"key: {key} \t val: {type(value)}") ... key: TABLE_cdp_neighbor_detail_info val: <class 'dict'> >>>
going to the next level:
>>> for key, value in objectJson['TABLE_cdp_neighbor_detail_info'].items(): ... print(f"key: {key} \t val: {type(value)}") ... key: ROW_cdp_neighbor_detail_info val: <class 'list'>
OK..now I have one more key and then I probably have the stuff I care about.
I now know that this structure is a list.
objectJson['TABLE_cdp_neighbor_detail_info']['ROW_cdp_neighbor_detail_info']
Lets see how many rows we have:
print(len(objectJson['TABLE_cdp_neighbor_detail_info']['ROW_cdp_neighbor_detail_info'])) 5
Sometimes its easier to put that long thing that is a list into a new variable as you can see below I put into "results_list" and then I iterate over results list.
>>> results_list = objectJson['TABLE_cdp_neighbor_detail_info']['ROW_cdp_neighbor_detail_info']
>>> for row in results_list: ... print(row) ... print() ... {'ifindex': '824352435', 'device_id': 'mynexuscore-2', 'sysname': 'dc-core2', 'numaddr': '1', 'v4addr': '10.128.1.5', 'platform_id': 'N9K-C9372PX', 'capability': ['router', 'switch', 'Supports-STP-Dispute'], 'intf_id': 'mgmt0', 'port_id': 'mgmt0', 'ttl': '156', 'version': 'Cisco Nexus Operating System (NX-OS) Software, Version 7.0(3)I4(8a)', 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '1500', 'syslocation': 'snmplocation', 'num_mgmtaddr': '1', 'v4mgmtaddr': '10.128.1.5'} {'ifindex': '436207616', 'device_id': 'tracer-01', 'numaddr': '2', 'v4addr': ['192.168.254.50', '192.168.5.50'], 'platform_id': 'N3000', 'capability': 'router', 'intf_id': 'Ethernet1/1', 'port_id': 'Te3/0/1', 'ttl': '153', 'version': '6.5.2.5', 'version_no': 'v2', 'nativevlan': '250', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '4362246246', 'device_id': 'mynexuscore-2Standby', 'numaddr': '2', 'v4addr': '10.128.1.15', 'v6addr': 'fe80::5a97:bdff:bbbb:aaaa', 'platform_id': 'AIR-CT5520-K9', 'capability': 'host', 'intf_id': 'Ethernet1/3', 'port_id': 'TenGigabitEthernet0/0/1', 'ttl': '125', 'version': "Manufacturer's Name: Cisco Systems Inc. Product Name: Cisco Controller Product Version: 8.5.171.0 RTOS Version: 8.5.171.0 Bootloader Version: 8.1.102.0 Build Type: DATA + WPS", 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '436209152', 'device_id': 'WLC-1', 'numaddr': '2', 'v4addr': '10.128.1.16', 'v6addr': 'fe80::72e4:22ff:bbbb:aaaa', 'platform_id': 'AIR-CT5520-K9', 'capability': 'host', 'intf_id': 'Ethernet1/4', 'port_id': 'TenGigabitEthernet0/0/1', 'ttl': '158', 'version': "Manufacturer's Name: Cisco Systems Inc. Product Name: Cisco Controller Product Version:8.5.171.0 RTOS Version: 8.5.171.0 Bootloader Version: 8.1.102.0 Build Type: DATA + WPS", 'version_no': 'v2', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '0'} {'ifindex': '436205555', 'device_id': 'MDF-1', 'vtpname': 'thisguy', 'numaddr': '1', 'v4addr': '10.128.1.18', 'platform_id': 'cisco WS-C2960X-48FPD-L', 'capability': ['switch', 'IGMP_cnd_filtering'], 'intf_id': 'Ethernet1/5', 'port_id': 'TenGigabitEthernet1/0/1', 'ttl': '165', 'version': 'Cisco IOS Software, C2960X Software (C2960X-UNIVERSALK9-M), Version 15.2(2)E3, RELEASE SOFTWARE (fc3)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2015 by Cisco Systems, Inc.\nCompiled Wed 26-Aug-15 07:12 by prod_rel_team', 'version_no': 'v2', 'nativevlan': '1', 'duplexmode': 'full', 'mtu': '0', 'num_mgmtaddr': '1', 'v4mgmtaddr': '10.128.1.13'} >>>
Now you can see that each row in the list is a dictionary. Well we know how to process that now.
Lest look at the keys we have available to us
>>> for row in results_list: ... print(row.keys()) ... dict_keys(['ifindex', 'device_id', 'sysname', 'numaddr', 'v4addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'duplexmode', 'mtu', 'syslocation', 'num_mgmtaddr', 'v4mgmtaddr']) dict_keys(['ifindex', 'device_id', 'numaddr', 'v4addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'nativevlan', 'mtu', 'num_mgmtaddr']) dict_keys(['ifindex', 'device_id', 'numaddr', 'v4addr', 'v6addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'duplexmode', 'mtu', 'num_mgmtaddr']) dict_keys(['ifindex', 'device_id', 'numaddr', 'v4addr', 'v6addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'duplexmode', 'mtu', 'num_mgmtaddr']) dict_keys(['ifindex', 'device_id', 'vtpname', 'numaddr', 'v4addr', 'platform_id', 'capability', 'intf_id', 'port_id', 'ttl', 'version', 'version_no', 'nativevlan', 'duplexmode', 'mtu', 'num_mgmtaddr', 'v4mgmtaddr']) >>>
OK lets say I want platform and v4IP:
>>> for row in results_list: ... print(f"Platform: {row['platform_id']} IP: {row['v4addr']}") ... Platform: N9K-C9372PX IP: 10.128.1.5 Platform: N3000 IP: ['192.168.254.50', '192.168.5.50'] Platform: AIR-CT5520-K9 IP: 10.128.1.15 Platform: AIR-CT5520-K9 IP: 10.128.1.16 Platform: cisco WS-C2960X-48FPD-L IP: 10.128.1.18 >>>
With all of this you can now see that some of the values are lists (devices with multiple IPs) so one more step to iterate over those if you need to.
Spend some time working through these exercises yourself. Knowing how to deal with these complex structures will serve you well!
errors:
this is like the first example.
06-10-2021 09:46 AM
Thanks. I havent ran through any complex data before. I've been relearning python due to the DevNet Associate and the OCG does not touch upon anything more than the basics. However, I did find the online DevNet learning labs does go into a little detail about nested data.
https://developer.cisco.com/learning/modules/intro-python/parsing-json-python/step/4
Between your post and this DevNet module, I think I may be able to achieve what I'm looking to do. Its taking quite a bit longer than I had hope for, but hopefully I run into more projects like this in the future and I can make use of this skill. Thanks!
Discover and save your favorite ideas. Come back to expert answers, step-by-step guides, recent topics, and more.
New here? Get started with these tips. How to use Community New member guide