Introduction to Python Classes – Part 3
Last week we talked about class design, and this week we will discuss how to package the code for reusability. Packaging the code is useful when working on a larger project. With large projects, it is wise to have a directory structure that will allow the project to scale if additional code is required in the future.
Typically, I try to put each class in its own file. This isn’t necessary for code implementation, but it helps structure the code for future developers who might want to take a look at this project to find more easily where each class is defined.
Let’s look to see what classes we have defined so far over the past two tutorials.
Past Code
# ip_man.py
import os
import requests
class IpManager:
"""Class to assign IP prefixes in Nautobot via REST API"""
def __init__(self):
self.base_url = "https://demo.nautobot.com/api"
_token = self._get_token()
self.headers = {
"Accept": "application/json",
"Authorization": f"Token {_token}",
}
@staticmethod
def _get_token():
"""Method to retrieve Nautobot authentication token"""
return os.environ["NAUTOBOT_TOKEN"]
def get_prefix(self, prefix_filter):
"""Method to retrieve a prefix from Nautobot
Args:
prefix_filter (dict): Dictionary supporting a Nautobot filter
Returns:
obj: Requests object containing Nautobot prefix
"""
url = f"{self.base_url}/ipam/prefixes/"
response = requests.get(url=url, headers=self.headers, params=prefix_filter)
return response
def new_prefix(self, parent_prefix_id, new_prefix):
"""Method to add a new prefix within a parent prefix to Nautobot
Args:
parent_prefix_id (str): UUID identifying a parent prefix
new_prefix (dict): Dictionary defining new prefix
Returns:
obj: Requests object containing new Nautobot prefix
>>>
"""
url = f"{self.base_url}/ipam/prefixes/{parent_prefix_id}/available-prefixes/"
body = new_prefix
response = requests.post(url=url, headers=self.headers, json=body)
return response
def get_available_ips(self, prefix_id):
"""Method to retrieve unused available IP addresses within a prefix
Args:
prefix_id (str): UUID identifying a prefix
Returns:
obj: Request object containing list of available IP addresses within Nautobot prefix
"""
url = f"{self.base_url}/ipam/prefixes/{prefix_id}/available-ips/"
response = requests.get(url=url, headers=self.headers)
return response
def delete_prefix(self, prefix_id):
"""Method to delete a Nautobot prefix
Args:
prefix_id (str): UUID identifying a prefix
Returns:
None
"""
url = f"{self.base_url}/ipam/prefixes/{prefix_id}/"
response = requests.delete(url=url, headers=self.headers)
return response
# prefix.py
class Prefix:
"""Class to represent network prefixes"""
def __init__(self, id, prefix, **kwargs):
"""Accepts Nautobot prefix dictionaries as input"""
self.id = id
self.prefix = prefix
def __str__(self):
"""Represents this object as a string for pretty-printing"""
return f"<{self.__class__.__name__} {self.prefix}>"
def __repr__(self):
"""Represents this object as a string for use in the REPL"""
return f"<{self.__class__.__name__} (id={self.id}, prefix={self.prefix})>"
def __eq__(self, other):
"""Used to determine if two objects of this class are equal to each other"""
return bool(other.id == self.id)
def __hash__(self):
"""Used to calculate a unique hash for this object, in case we ever want to use it in a dictionary or a set"""
return hash(self.id)
Restructuring the Files
So far, we have defined two classes, IpManager and Prefix. I would place each of these classes in its own file (ip_man.py and prefix.py). In order to create a package, we will restructure these files in the following format:
nautobot/
├── __init__.py
└── nautobot_ip_manager/
├── __init__.py
├── ip_man.py
└── prefix.py
I have this file structure in my /home/projects directory, but you can place these files wherever you wish.
Please note, you can change the folder names and structure above to whatever you wish. For this tutorial, we will continue with the Nautobot naming scheme.
Why So Many __init__.py
Files?
With packaging, each subdirectory of your code must contain an __init__.py
file in order for Python to understand that this directory contains code to package. These __init__.py
files do not have to contain code, but they may if that is what your project requires.
Importing Your Package
With our files now restructured, we can import our code as a package.
First, cd to the directory above Nautobot.
cd /home/projects/
Go ahead and launch your Python interpreter.
python3
To import our classes, we can use the following import statement:
from <package> import <classname>
such as:
>>> from nautobot.nautobot_ip_manager.prefix import Prefix
>>> from nautobot.nautobot_ip_manager.ip_man import IpManager
>>> ip_mgr = IpManager()
>>> r = ip_mgr.get_prefix({"prefix":"10.0.0.0/8"})
>>> nautobot_prefix_data = r.json()["results"]
>>> prefix_objects = [Prefix(**item) for item in nautobot_prefix_data]
>>> prefix_objects
[<Prefix (id=08dabdef-26f1-4389-a9d7-4126da74f4ec, prefix=10.0.0.0/8)>, <Prefix (id=b4e452a2-25b2-4f26-beb5-dbe2bc2af868, prefix=10.0.0.0/8)>]
>>> print(prefix_objects[0])
<Prefix 10.0.0.0/8>
Another way to import a file is to use the import statement directly.
>>> import nautobot.nautobot_ip_manager.prefix
>>> import nautobot.nautobot_ip_manager.ip_man
>>> ip_mgr = nautobot.nautobot_ip_manager.ip_man.IpManager()
>>> r = ip_mgr.get_prefix({"prefix":"10.0.0.0/8"})
>>> nautobot_prefix_data = r.json()["results"]
>>> prefix_objects = [nautobot.nautobot_ip_manager.prefix.Prefix(**item) for item in nautobot_prefix_data]
>>> prefix_objects
[<Prefix (id=08dabdef-26f1-4389-a9d7-4126da74f4ec, prefix=10.0.0.0/8)>, <Prefix (id=b4e452a2-25b2-4f26-beb5-dbe2bc2af868, prefix=10.0.0.0/8)>]
>>> print(prefix_objects[0])
<Prefix 10.0.0.0/8>
What if I don’t want to store each class in its own file?
Storing each class in its own file helps with code structure, but it may become cumbersome when trying to import many classes.
Another way to structure the code is to place each class in the __init__.py
file under nautobot_ip_manager.
nautobot/
├── __init__.py
└── nautobot_ip_manager/
└── __init__.py
# __init__.py
import os
import requests
class IpManager:
"""Class to assign IP prefixes in Nautobot via REST API"""
def __init__(self):
self.base_url = "https://demo.nautobot.com/api"
_token = self._get_token()
self.headers = {
"Accept": "application/json",
"Authorization": f"Token {_token}",
}
@staticmethod
def _get_token():
"""Method to retrieve Nautobot authentication token"""
return os.environ["NAUTOBOT_TOKEN"]
def get_prefix(self, prefix_filter):
"""Method to retrieve a prefix from Nautobot
Args:
prefix_filter (dict): Dictionary supporting a Nautobot filter
Returns:
obj: Requests object containing Nautobot prefix
"""
url = f"{self.base_url}/ipam/prefixes/"
response = requests.get(url=url, headers=self.headers, params=prefix_filter)
return response
def new_prefix(self, parent_prefix_id, new_prefix):
"""Method to add a new prefix within a parent prefix to Nautobot
Args:
parent_prefix_id (str): UUID identifying a parent prefix
new_prefix (dict): Dictionary defining new prefix
Returns:
obj: Requests object containing new Nautobot prefix
>>>
"""
url = f"{self.base_url}/ipam/prefixes/{parent_prefix_id}/available-prefixes/"
body = new_prefix
response = requests.post(url=url, headers=self.headers, json=body)
return response
def get_available_ips(self, prefix_id):
"""Method to retrieve unused available IP addresses within a prefix
Args:
prefix_id (str): UUID identifying a prefix
Returns:
obj: Request object containing list of available IP addresses within Nautobot prefix
"""
url = f"{self.base_url}/ipam/prefixes/{prefix_id}/available-ips/"
response = requests.get(url=url, headers=self.headers)
return response
def delete_prefix(self, prefix_id):
"""Method to delete a Nautobot prefix
Args:
prefix_id (str): UUID identifying a prefix
Returns:
None
"""
url = f"{self.base_url}/ipam/prefixes/{prefix_id}/"
response = requests.delete(url=url, headers=self.headers)
return response
class Prefix:
"""Class to represent network prefixes"""
def __init__(self, id, prefix, **kwargs):
"""Accepts Nautobot prefix dictionaries as input"""
self.id = id
self.prefix = prefix
def __str__(self):
"""Represents this object as a string for pretty-printing"""
return f"<{self.__class__.__name__} {self.prefix}>"
def __repr__(self):
"""Represents this object as a string for use in the REPL"""
return f"<{self.__class__.__name__} (id={self.id}, prefix={self.prefix})>"
def __eq__(self, other):
"""Used to determine if two objects of this class are equal to each other"""
return bool(other.id == self.id)
def __hash__(self):
"""Used to calculate a unique hash for this object, in case we ever want to use it in a dictionary or a set"""
return hash(self.id)
We will be able to import both of these classes now using one import statement.
>>> from nautobot.nautobot_ip_manager import Prefix, IpManager
>>> Prefix
<class 'nautobot.nautobot_ip_manager.Prefix'>
>>> IpManager
<class 'nautobot.nautobot_ip_manager.IpManager'>
Conclusion
Packaging your code helps with scalability and code reusability. Without packaging, working on a larger project can devolve into a giant mess. I hope this helps with understanding how packaging works!
Thanks!
-Khurram
Tags :
Contact Us to Learn More
Share details about yourself & someone from our team will reach out to you ASAP!