Custom Subclasses

If the file format you are looking for is not available you do have the option to create a custom subclass (also if you think it's useful please upstream it back to the project).

Base class

First let's take a look at the base Resource class:

class Resource(ABC):
    """Base class to be inherited from and extended to suit specific resource.

    Attributes
    ----------
    label : (str)
        Human readable name for resource and used with extension in files name.
    extensions : (str)
        The extension of the filetype being downloaded

    location : (str)
        The path or URL to the resource that needs to be downloaded & installed

    arguments : (list|bool)
        Specify any arguments to be passed on installation, False indicates no arguments.

    downloaded : (bool)
        Used to delineate if Resource is downloaded, if using local file set to True, else leave as False.

    Methods
    -------
    download:
        Downloads Resource from location specified in self.location of the instance
    install (abstract):
        Subclass implemented function for how to install/configure resource once downloaded.

    Examples
    --------
    Subclassing a static resource class to download static assets (images, videos etc.)
    \`\`\`
    class StaticResource(Resource):
        def __init__(self, label, extension, location, arguments = False, downloaded = False):
            super().__init__(label, extension, location, arguments, downloaded)
        def install(self):
            logging.info("No installation necessary for StaticResources")
            pass
    \`\`\`
    """
    def __init__(self, label, extension, location, arguments = False, downloaded = False):

        self.label = label
        self.extension = extension
        self.location = location
        self.arguments = arguments
        self.downloaded = downloaded

    def download(self, file_path = False):
        """Downloads Resource from location specified in self.location of the instance.

        Attributes
        ----------
        file_path : (str|bool)
            The path to where the resource should download to. 
            Leave as false for download folder + name + extension.
            NOTE: Custom paths MUST include extension.

        Examples
        --------
        \`\`\`
        python = EXEResource('python-installer', 'https://www.python.org/ftp/python/3.8.1/python-3.8.1.exe')
        python.download() # Downloads python installer exe to OS downloads folder
        \`\`\`
        """
        logging.info(f"Downloading {self.label}")

        if not file_path:
            file_path = f"{DOWNLOAD_FOLDER}{os.sep}{self.label}{self.extension}"

        if os.path.exists(file_path): # If file already exists
            self.downloaded = True
            self.location = file_path
            return

        logging.info("Starting binary download")
        file_content = requests.get(self.location)
        open(file_path, 'wb').write(file_content.content) # Save file
        # TODO: Error catching
        self.downloaded = True
        self.location = file_path

    @abstractmethod
    def install(self) -> None:
        """installation steps after all necessary downloads are completed"""
        raise NotImplementedError

As we can see it is an abstract class with a few attributes, and one abstract method that must be implemented in any sub-classes to work.

Minimum requirements for subclass

The convention for sub-classes is to use the capitalized extension + Resource for the name (i.e. for ".exe" use EXEResource etc.), also classes are documented using a modified numpy style docstring. There are a few things that you should do minimally to create the subclass: 1. Create a __init__() method. You can add any valuable extra attributes here such as whether the file should be removed after installation with a remove flag etc. 2. Override the install() method; this is where you will put any logic that needs to run to install/modify the downloaded resources.

Example Subclass

Let's take a look at the ZIPResource subclass as an example. The intention with this subclass is to download a .zip file and extract all the files into the folder it was downloaded in:

class ZIPResource(Resource):
    """Used to download and extract .zip files.

    Attributes
    ----------
    label : (str)
        Human readable name for resource and used with extension in files name.

    location : (str)
        The path or URL to the resource that needs to be downloaded & installed

    arguments : (list|bool)
        Specify any arguments to be passed on installation, False indicates no arguments.

    downloaded : (bool)
        Used to delineate if Resource is downloaded, if using local file set to True, else leave as False.
    remove: (bool)
        Whether to delete the .zip after installation, by default True.

    Methods
    -------
    download:
        Downloads Resource from location specified in self.location of the instance
    install:
        Extracts the .zip file.
        NOTE: assumes you have already downloaded the file or set the self.location to correct file path.

    Examples
    --------
    \`\`\`
    from pystall.core import ZIPResource, build
    micro = ZIPResource("micro editor", "https://github.com/zyedidia/micro/releases/download/v1.4.1/micro-1.4.1-win64.zip")
    build(micro)
    \`\`\`
    """
    def __init__(self, label, location, arguments = False, downloaded = False, remove = True):
        super().__init__(label, ".zip", location, arguments, downloaded)
        self.remove = remove

    def extract(self):
        """Extracts the .zip file."""
        extract_path = self.location[:-3:]
        logging.info(f"Extracting Zip archive {self.location} to {extract_path}")
        with ZipFile(self.location, "r") as archive:
                archive.extractall(extract_path) # Strip extension and extract to folder

    def install(self):
        """Extracts the .zip file and runs necessary installation steps. NOTE: Not yet implemented"""
        if self.downloaded:
            logging.info(f"Installing {self.label}")
            self.extract()

        else:
            logging.error(f"{self.name} failed to install due to not being downloaded")

        if self.remove:
            logging.info(f"Removing installer {self.label}")
            os.remove(self.location)

When someone creates an instance of the ZIPResource class and runs the build() method on it, it will call the download() and install() methods. The download method is rarely needed to be overridden, but the install() method is an abstract method and MUST be overridden.