Python Dispatcher¶
At its core, a dispatcher is a decision-making mechanism. Just as a traffic officer directs vehicles based on traffic conditions, a dispatcher in the programming world directs tasks, calls, or requests based on certain criteria. Its primary function is to decide how to route or handle incoming information or requests, ensuring that the right action is taken for each input.
Common Dispatcher Use Cases¶
- Many if-else statements - which can become unwieldy and difficult to manage, dispatchers provide a structured way to make decisions.
- Efficiency: In some patterns, dispatching can be done in constant time (like with dictionaries), making it faster than iterating through lists or conditions.
- Flexibility: Dispatcher patterns, especially those based on dictionaries or objects, can be easily extended or modified without changing the core logic of the application.
- Clear Separation of Concerns: The logic for deciding and the logic for executing are kept separate, leading to cleaner and more maintainable code.
- Explicit: The dictionary dispatcher is explicit on what to do.
Simple Dispatcher Example¶
One of the most common ways to know that a dispatcher would be useful, is a series of if-else statements are being used.
def cisco_ios_actions():
print("Here are Cisco IOS Actions")
def cisco_nxos_actions():
print("Here are Cisco NXOS Actions")
def arista_eos_actions():
print("Here are Arista EOS Actions")
def juniper_junos_actions():
print("Here are Juniper JUNOS Actions")
operating_system = "cisco_ios"
if operating_system == "cisco_ios":
cisco_ios_actions()
elif operating_system == "cisco_nxos":
cisco_nxos_actions()
elif operating_system == "arista_eos":
arista_eos_actions()
elif operating_system == "juniper_junos":
juniper_junos_actions()
Since everything in Python is an object, including a function, this can easily be converted into a dictionary.
func_mapper = {
"cisco_ios": cisco_ios_actions,
"cisco_nxos": cisco_nxos_actions,
"arista_eos": arista_eos_actions,
"juniper_junos": juniper_junos_actions,
}
operating_system = "cisco_ios"
func_mapper[operating_system]()
Simple Dispatcher Example - LAB¶
In this short lab, go through and try all 4 options of the dispatcher.
Dispatcher Default¶
Often times you will require a default, which can also be handled via a dictionary using the .get method with a default value.
Dispatcher Default - LAB¶
Using the dictionary method, build an equivalent solution to the below capability's.
def cisco_ios_actions():
print("Here are Cisco IOS Actions")
def cisco_nxos_actions():
print("Here are Cisco NXOS Actions")
def arista_eos_actions():
print("Here are Arista EOS Actions")
def juniper_junos_actions():
print("Here are Juniper JUNOS Actions")
def cisco_actions():
print("Here are the generic Cisco Actions")
operating_system = "cisco_ios"
if operating_system == "cisco_ios":
cisco_ios_actions()
elif operating_system == "cisco_nxos":
cisco_nxos_actions()
elif operating_system == "arista_eos":
arista_eos_actions()
elif operating_system == "juniper_junos":
juniper_junos_actions()
else:
cisco_actions()
Dispatcher Variables¶
Variables can also be sent within the callable just as:
Dispatcher Variables - LAB¶
Using the dictionary method, build an equivalent solution to the below capability's.
def cisco_ios_actions(device):
print(f"Here are Cisco IOS Actions for {device}")
def cisco_nxos_actions(device):
print(f"Here are Cisco NXOS Actions for {device}")
def arista_eos_actions(device):
print(f"Here are Arista EOS Actions for {device}")
def juniper_junos_actions(device):
print(f"Here are Juniper JUNOS Actions for {device}")
def cisco_actions(device):
print(f"Here are the generic Cisco Actions for {device}")
operating_system = "cisco_ios"
device = "nyc-rt01"
if operating_system == "cisco_ios":
cisco_ios_actions(device)
elif operating_system == "cisco_nxos":
cisco_nxos_actions(device)
elif operating_system == "arista_eos":
arista_eos_actions(device)
elif operating_system == "juniper_junos":
juniper_junos_actions(device)
else:
cisco_actions(device)
Dispatcher Variables KWARGS¶
Sometimes the function signature will not always be the same, it may be easier to simply send kwargs, and build your dictionary.
Dispatcher Variables KWARGS - LAB¶
Using the dictionary method, build an equivalent solution to the below capability's.
def cisco_ios_actions(device):
print(f"Here are Cisco IOS Actions for {device}")
def cisco_nxos_actions(device):
print(f"Here are Cisco NXOS Actions for {device}")
def arista_eos_actions(device, port=22):
print(f"Here are Arista EOS Actions for {device} on port {str(port)}")
def juniper_junos_actions(device):
print(f"Here are Juniper JUNOS Actions for {device}")
def cisco_actions(device):
print(f"Here are the generic Cisco Actions for {device}")
operating_system = "arista_eos"
device = "nyc-rs01"
port = 443
if operating_system == "cisco_ios":
cisco_ios_actions(device)
elif operating_system == "cisco_nxos":
cisco_nxos_actions(device)
elif operating_system == "arista_eos":
arista_eos_actions(device, port)
elif operating_system == "juniper_junos":
juniper_junos_actions(device)
else:
cisco_actions(device)
Dispatcher Ambiguity¶
Sometimes the current if/else conditionals will be more complex than a explicit match, however, that is often able to be consolidated.
Dispatcher Ambiguity - LAB¶
Using the dictionary method, build an equivalent solution to the below capability's.
def cisco_actions(device):
print(f"Here are Cisco Actions for {device}")
def arista_eos_actions(device, port=22):
print(f"Here are Arista EOS Actions for {device} on port {str(port)}")
def juniper_junos_actions(device):
print(f"Here are Juniper JUNOS Actions for {device}")
operating_system = "cisco_ios" # or cisco_ios
device = "nyc-rs01"
if operating_system.startswith("cisco_ios"):
cisco_actions(device)
elif operating_system == "arista_eos":
arista_eos_actions(device, port)
elif operating_system == "juniper_junos":
juniper_junos_actions(device)
Dispatcher import_string¶
This concept is lifted from Django's import_string, here is an equivalent version of it.
import importlib
def import_string(dotted_path):
module_name, class_name = dotted_path.rsplit(".", 1)
return getattr(importlib.import_module(module_name), class_name)
Using netutils ip methods, we can see how this works in action. The important part to note is that the "dotted_path" is the string representation of the full import path. So if you were to do from netutils.ip import ip_addition that would be netutils.ip.ip_addition.
>>> import_string("netutils.ip.ip_addition")("10.10.10.100", 5)
'10.10.10.105'
>>> import_string("netutils.ip.ip_subtract")("10.10.10.100", 5)
'10.10.10.95'
>>>
That is to say, that you return the callable, and then provide the functions to said callable.
Dispatcher import_string - LAB¶
Using the same import_string function, develop the equivalent of the below dictionary based mapping.
parser_map: t.Dict[str, t.Type[parser.BaseConfigParser]] = {
"arista_eos": parser.EOSConfigParser,
"cisco_ios": parser.IOSConfigParser,
"cisco_nxos": parser.NXOSConfigParser,
"juniper_junos": parser.JunosConfigParser,
}
bgp_config = '''router bgp 65250
bgp log-neighbor-changes
no bgp default ipv4-unicast
neighbor 10.1.12.1 remote-as 65100'''
The function should be able to take in a dotted_path, the bgp_config and return a parsed configuration.
Warning
The configuration provided has only been tested with Cisco OS's.