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
<span role="button" tabindex="0" data-code="# 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"
# 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:

<span role="button" tabindex="0" data-code="from <package> import
from <package> import <classname>

such as:

<span role="button" tabindex="0" data-code=">>> 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])
>>> 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.

<span role="button" tabindex="0" data-code=">>> 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])
>>> 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
<span role="button" tabindex="0" data-code="# __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"
# __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.

<span role="button" tabindex="0" data-code=">>> from nautobot.nautobot_ip_manager import Prefix, IpManager >>> Prefix <class 'nautobot.nautobot_ip_manager.Prefix'> >>> IpManager
>>> 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



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Author