text: #! /usr/bin/env python
  # -*- coding: utf-8 -*-
  
  """Package Module
  
  Utilities for managing Python packages.
  
  .. moduleauthor:: Timothy Helton <timothy.j.helton@gmail.com>
  """
  
  from collections import namedtuple
  from datetime import datetime as dt
  import glob
  import subprocess
  import os
  import os.path as osp
  import tarfile
  from typing import Iterable
  
  from strumenti import system
  
  logger = system.logger_setup(name=__name__)
  
  
  class Manage:
      """Class will assist with managing Python packages.
  
      :Attributes:
  
          - **outdated**: *dict* outdated packages
          - **packages**: *dict* all packages
          - **req_txt_path**: *str* path to the file containing all package \
              version requirements
          - **wheels**: *list* path to all wheels generated by installing \
              packages (this includes all dependencies)
          - **wheel_path**: *str* path to the temporary wheelhouse directory
      """
      def __init__(self):
          self._outdated = {}
          self._packages = {}
          self.req_txt_path = 'requirements.txt'
          self._wheels = []
          self.wheel_path = 'temp_wheelhouse'
  
      @property
      def outdated(self):
          self.list_packages(outdated=True)
          return self._outdated
  
      @property
      def packages(self):
          self.list_packages()
          return self._packages
  
      @property
      def wheels(self):
          self.get_wheels()
          return self._wheels
  
      def __repr__(self):
          return 'Manage()'
  
      def create_tar(self):
          """Create tar file of wheels."""
          with tarfile.open('{}.tar'.format(self.wheel_path), 'w:gz') as tar:
              for name in self.wheels:
                  tar.add(name)
  
      def create_wheels(self, packages: ([str], [tuple], [list])):
          """Create wheels for packages.
  
          :param packages: names of packages to upgrade
          :type: str tuple list
          """
          if not self._packages:
              self.list_packages()
  
          packages = system.check_list(packages)
  
          os.makedirs(self.wheel_path, exist_ok=True)
          for pkg in packages:
              logger.info('{:15}{}'.format('Create Wheel:', pkg))
              version = self._packages[pkg]
              proc = subprocess.Popen(['pip', 'wheel',
                                       '--wheel-dir={}'.format(self.wheel_path),
                                       '{}=={}'.format(pkg, version)],
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
              output, error = proc.communicate()
              logger.debug(output.decode())
              if error:
                  logger.error(error.decode())
              logger.info('{:15}{}'.format('Complete:', pkg))
  
      def get_wheels(self):
          """Get the absolute path for all wheel files."""
          wheels = glob.glob('{}{}*'.format(self.wheel_path, os.sep))
          self._wheels = [osp.realpath(x) for x in wheels]
  
      def list_packages(self, outdated: bool=False):
          """Get information on installed packages.
  
          :param bool outdated: if True only outdated packages will be retrieved
          """
          versions = namedtuple('Versions', ['old_ver', 'new_ver'])
  
          cmd = ['pip', 'list']
          if outdated:
              cmd.append('-o')
  
          execute = subprocess.run(cmd, stdout=subprocess.PIPE)
          packages = execute.stdout.decode('utf-8').splitlines()
          packages = [x.split() for x in packages]
  
          if outdated:
              if not packages:
                  logger.info('All installed packages are up to date.')
              else:
                  self._outdated = {x[0]: versions(x[2], x[4]) for x in packages}
          else:
              self._packages = {x[0]: x[1] for x in packages}
  
      @staticmethod
      def install_packages(packages: Iterable, upgrade: bool=True):
          """Install Python packages.
  
          .. note:: The argument packages can be a list of package names as \
              strings or tuples of package names with desired versions to be \
              installed.
  
              - List of Strings example:
                  - ['numpy', 'scipy']
              - List of Tuples example:
                  - [('numpy', '1.10.4'), ('scipy', '0.16.1')]
  
          :param iterable packages: packages to install
          :param bool upgrade: set to True if package is already installed \
              and the version is being upgraded or False if a new package is \
              being installed
          """
          cmd = ['pip', 'install']
          if upgrade:
              cmd.append('--upgrade')
  
          for pkg in iter(packages):
              if isinstance(pkg, tuple):
                  pkg = '{}=={}'.format(pkg[0], pkg[1])
  
              logger.info('\n{:10}{}'.format('Install:', pkg))
              cmd.append(pkg)
              proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
              output, error = proc.communicate()
              logger.debug(output.decode())
              if error:
                  logger.error(error.decode())
              if not error:
                  logger.info('{:10}\n'.format('Complete'))
  
      def update_packages(self):
          """Update to latest version of all packages."""
          update_pkgs = self.outdated.keys()
          self.install_packages(update_pkgs)
          if update_pkgs:
              self.update_requirements()
  
      def update_requirements(self):
          """Update the requirements.txt file.
  
          .. note:: The requirements file will only be updated if the path is \
              defined in the attribute req_txt_file.
          """
          try:
              execute = subprocess.run(['pip', 'freeze'], stdout=subprocess.PIPE)
              with open(self.req_txt_path, 'w') as f:
                  f.write(execute.stdout.decode('utf-8'))
              logger.info(('\nUpdated Requirements file: {}'
                           '\n').format(self.req_txt_path))
          except IOError:
              logger.error('\nFile Not Found: {}\n'
                           'Requirements File Not Updated'
                           '\n').format(self.req_txt_path)
  
  
  if __name__ == '__main__':
      log_file = 'packages_{}.log'.format(dt.date(dt.now()))
      logger = system.logger_setup(name=__name__, log_file=log_file)
      logger.info('Checking for package updates')
      p = Manage()
      p.update_packages()
      logger.info('Package updates complete')
  