text: from collections import namedtuple
  from contextlib import contextmanager
  import os
  import shutil
  import tempfile
  
  # pylint: disable=unused-import
  
  from typing import Any, Dict, Iterable, Iterator, List, Tuple, Type
  
  import yaml
  
  from sheepdoge.config import Config
  from sheepdoge.exception import SheepdogeInvalidPupTypeException
  from sheepdoge.utils import ShellRunner
  
  # The character used in the `location` in the pupfile to split between
  # `pup_type` and `path`.
  LOCATION_SPLIT_CHAR = "+"
  
  PupfileEntry = namedtuple("PupfileEntry", "name path pup_type")
  
  
  def _pup_types_to_classes():
      # type: () -> Dict[str, Type[Pup]]
      return {"fs": FsPup, "galaxy": GalaxyPup, "git": GitPup}
  
  
  def _dependency_types_to_classes():
      # type: () -> Dict[str, Type[PupDependencies]]
      return {"txt": PythonDependencies, "yml": AnsibleDependencies}
  
  
  class Pup(object):
      """Container of all pup related logic. Contains both static methods as well
      as base methods inherited by the different types of pups.
      """
  
      PYTHON_DEP_FILE = "requirements.txt"
      ANSIBLE_DEP_FILE = "requirements.yml"
  
      def __init__(self, name, path, config=None):
          # type: (str, str, Config) -> None
          self._name = name
          self._path = path
          self._pup_dependencies = []  # type: List[PupDependencies]
  
          self._config = config or Config.get_config_singleton()
  
      @classmethod
      def parse_pupfile_into_pups(cls, pupfile_path):
          # type: (str) -> List[Pup]
          """Return a list of `Pup` objects for each pup we wish to install."""
          # Read in file and parse with yml
          with open(pupfile_path, "r") as pupfile:
              entries_from_file = cls.parse_text_into_entries(pupfile.read())
  
          return cls.create_from_entries(entries_from_file)
  
      @classmethod
      def parse_text_into_entries(cls, file_contents):
          # type: (str) -> List[PupfileEntry]
          """Given the text of `pupfile.yml`, return the structured data we will
          iterate through to create individual `Pup` instances.
  
          :param file_contents: A string containing the contents of `pupfile.yml`.
          :return: A list of structured pupfile entries.
          """
          entries = []
  
          for dict_entry in yaml.load(file_contents):
              name = dict_entry["name"]
              pup_type, path = cls._parse_location(dict_entry["location"])
  
              if pup_type not in _pup_types_to_classes().keys():
                  err_msg = "{} is not a valid pup type.".format(pup_type)
                  raise SheepdogeInvalidPupTypeException(err_msg)
              entries.append(PupfileEntry(name=name, path=path, pup_type=pup_type))
  
          return entries
  
      @staticmethod
      def _parse_location(location):
          # type: (str) -> Tuple[str, str]
          split_location = location.split(LOCATION_SPLIT_CHAR)
  
          return split_location[0], split_location[1]
  
      @classmethod
      def create_from_entries(cls, entries):
          # type: (Iterable[PupfileEntry]) -> List[Pup]
          """Create a pup based on a line in a `pupfile.yml`.
  
          Instantiates and returns a subclass of `Pup` for the specific type of
          pup we are installing (i.e. from local filesystem, git, ansible-galaxy).
  
          :param entries: The input entries from the parsed pupfile.
          :return: The specific pup instances we are installing.
          """
          pups = []
  
          for entry in entries:
              pup_cls = _pup_types_to_classes()[entry.pup_type]
              pup = pup_cls(entry.name, entry.path)
              pups.append(pup)
  
          return pups
  
      def to_dict(self):
          # type: () -> Dict[str, Any]
          """Return a readable version of the pup"""
          # @TODO(mattjmcnaughton) Determine the best long term solution. Is it
          # defining `__repr__` or maybe `__eq__`?
          return {"name": self._name, "pup_type": self.__class__, "path": self._path}
  
      def install(self):
          # type: () -> None
          """Install all aspects of the pup.
          """
          pup_in_kennel_path = self._install_pup()
          self._install_pup_dependencies(pup_in_kennel_path)
  
      def _install_pup(self):
          # type: () -> str
          """Download the pup onto the local file system."""
          raise NotImplementedError
  
      def _install_pup_dependencies(self, pup_in_kennel_path):
          # type: (str) -> None
          """Install the dependencies for the pup (i.e. other ansible roles or
          python packages.
          """
          python_dependency_file, ansible_dependency_file = self._get_dep_files(
              pup_in_kennel_path
          )
  
          for dep_file_path in {python_dependency_file, ansible_dependency_file}:
              if dep_file_path is not None:
                  dependencies = PupDependencies.create_from_dep_file_path(dep_file_path)
                  dependencies.install()
  
      @classmethod
      def _get_dep_files(cls, pup_in_kennel_path):
          # type: (str) -> Tuple[str, str]
          python_dep_file = os.path.join(pup_in_kennel_path, cls.PYTHON_DEP_FILE)
          ansible_dep_file = os.path.join(pup_in_kennel_path, cls.ANSIBLE_DEP_FILE)
  
          if not os.path.isfile(python_dep_file):
              python_dep_file = None
  
          if not os.path.isfile(ansible_dep_file):
              ansible_dep_file = None
  
          return python_dep_file, ansible_dep_file
  
      @staticmethod
      @contextmanager
      def _with_tmp_dir():
          # type: () -> Iterator[str]
          tmp_dir_path = tempfile.mkdtemp()
  
          yield tmp_dir_path
  
          shutil.rmtree(tmp_dir_path)
  
  
  class FsPup(Pup):
      """A pup for which the source code is already on the local file system (we
      predominantly use this pup for testing).
  
      All fs pupfile locations should be specified relative to the location of the
      pupfile.
      """
  
      def _install_pup(self):
          # type: () -> str
          fs_pup_location = os.path.join(self._config.get("abs_pupfile_dir"), self._path)
  
          pup_in_kennel_path = os.path.join(
              self._config.get("abs_kennel_roles_dir"), self._name
          )
  
          shutil.copytree(fs_pup_location, pup_in_kennel_path)
  
          return pup_in_kennel_path
  
  
  class GitPup(Pup):
      """A pup for which the source lives in a remote git repo.
      """
  
      def _install_pup(self):
          # type: () -> str
          with self._with_tmp_dir() as install_dir:
              install_path = os.path.join(install_dir, self._name)
  
              git_cmd = ["git", "clone", self._path, install_path]
  
              ShellRunner(git_cmd).run()
  
              pup_in_kennel_path = os.path.join(
                  self._config.get("abs_kennel_roles_dir"), self._name
              )
  
              shutil.copytree(install_path, pup_in_kennel_path)
  
              return pup_in_kennel_path
  
  
  class GalaxyPup(Pup):
      """A pup for which the source lives on ansible-galaxy.
      """
  
      def _install_pup(self):
          # type: () -> str
          with self._with_tmp_dir() as install_dir:
              ansible_galaxy_cmd = [
                  "ansible-galaxy",
                  "install",
                  self._path,
                  "-p",
                  install_dir,
              ]
  
              ShellRunner(ansible_galaxy_cmd).run()
  
              role_name = os.listdir(install_dir)[0]
  
              install_path = os.path.join(install_dir, role_name)
  
              pup_in_kennel_path = os.path.join(
                  self._config.get("abs_kennel_roles_dir"), self._name
              )
  
              shutil.copytree(install_path, pup_in_kennel_path)
  
              return pup_in_kennel_path
  
  
  class PupDependencies(object):
      """A base representation of a collection of pup dependencies. Either
      python packages or ansible roles.
      """
  
      @staticmethod
      def create_from_dep_file_path(dep_file_path):
          # type: (str) -> PupDependencies
          """Given the path to a pup's dependency file, instantiate an instance of
          the appropriate subclass based on the given `dep_file_path`.
  
          :param dep_file_path: The path to the dependency file.
          :return: Instance of subclass of `PupDependencies`.
          """
          file_extension = dep_file_path.split(".")[-1]
  
          return _dependency_types_to_classes()[file_extension](dep_file_path)
  
      def __init__(self, dep_file_path, config=None):
          # type: (str, Config) -> None
          self._dep_file_path = dep_file_path
  
          self._config = config or Config.get_config_singleton()
  
      def install(self):
          # type: () -> None
          """Install the pup dependency."""
          raise NotImplementedError
  
  
  class PythonDependencies(PupDependencies):
      """Pip packages that must be installed on local machine for this kennel to
      run.
      """
  
      def install(self):
          # type: () -> None
          pip_cmd = ["pip", "install", "-r", self._dep_file_path]
  
          ShellRunner(pip_cmd).run()
  
  
  class AnsibleDependencies(PupDependencies):
      """Ansible roles that must be installed on local machine for this kennel
      to run.
      """
  
      def install(self):
          # type: () -> None
          ansible_galaxy_cmd = [
              "ansible-galaxy",
              "install",
              "-r",
              self._dep_file_path,
              "-p",
              self._config.get("abs_kennel_roles_dir"),
          ]
  
          ShellRunner(ansible_galaxy_cmd).run()
  