From 02cedc0a6104bb27722ce3d9ef25f161157619de Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Tue, 23 Oct 2018 11:02:30 +0200 Subject: [PATCH] Start refactoring to use python_fb_tools --- bin/create-vmware-template | 41 +- lib/cr_vmware_tpl/__init__.py | 2 +- lib/cr_vmware_tpl/app.py | 812 ++++++---------------------------- python_fb_tools | 2 +- requirements.txt | 1 + 5 files changed, 161 insertions(+), 697 deletions(-) diff --git a/bin/create-vmware-template b/bin/create-vmware-template index f0eab74..38e7507 100755 --- a/bin/create-vmware-template +++ b/bin/create-vmware-template @@ -1,24 +1,39 @@ #!/usr/bin/env python3 +from __future__ import print_function # Standard modules import sys + +if sys.version_info[0] != 3: + print("This script is intended to use with Python3.", file=sys.stderr) + print("You are using Python: {0}.{1}.{2}-{3}-{4}.\n".format( + *sys.version_info), file=sys.stderr) + sys.exit(1) + +if sys.version_info[1] < 4: + print("A minimal Python version of 3.4 is necessary to execute this script.", file=sys.stderr) + print("You are using Python: {0}.{1}.{2}-{3}-{4}.\n".format( + *sys.version_info), file=sys.stderr) + sys.exit(1) + import os import logging +import pathlib import locale -# own modules: -cur_dir = os.getcwd() -base_dir = cur_dir +from pathlib import Path + +# get own modules: + +my_path = Path(__file__) +my_real_path = my_path.resolve() +bin_path = my_real_path.parent +base_dir = bin_path.parent +lib_dir = base_dir.joinpath('lib') +module_dir = lib_dir.joinpath('cr_vmware_tpl') -if sys.argv[0] != '' and sys.argv[0] != '-c': - bin_dir = os.path.dirname(os.path.realpath(sys.argv[0])) -else: - bin_dir = os.path.dirname(os.path.realpath(__file__)) -base_dir = os.path.abspath(os.path.join(bin_dir, '..')) -lib_dir = os.path.join(base_dir, 'lib') -module_dir = os.path.join(lib_dir, 'cr_vmware_tpl') -if os.path.exists(module_dir): - sys.path.insert(0, lib_dir) +if module_dir.exists(): + sys.path.insert(0, str(lib_dir)) from cr_vmware_tpl.app import CrTplApplication @@ -37,7 +52,7 @@ app.initialized = True if app.verbose > 2: print("{c}-Object:\n{a}".format(c=app.__class__.__name__, a=app)) -app() +# app() sys.exit(0) diff --git a/lib/cr_vmware_tpl/__init__.py b/lib/cr_vmware_tpl/__init__.py index 771370f..35f5394 100644 --- a/lib/cr_vmware_tpl/__init__.py +++ b/lib/cr_vmware_tpl/__init__.py @@ -1,6 +1,6 @@ #!/bin/env python3 # -*- coding: utf-8 -*- -__version__ = '0.1.2' +__version__ = '0.2.1' # vim: ts=4 et list diff --git a/lib/cr_vmware_tpl/app.py b/lib/cr_vmware_tpl/app.py index 2833491..284e554 100644 --- a/lib/cr_vmware_tpl/app.py +++ b/lib/cr_vmware_tpl/app.py @@ -23,25 +23,32 @@ import getpass # Own modules from . import __version__ as GLOBAL_VERSION -from .errors import PpAppError +import fb_tools +from fb_tools.app import BaseApplication +from fb_tools.errors import FbAppError, ExpectedHandlerError, CommandNotFoundError -from .common import terminal_can_colors -from .common import caller_search_path +from fb_tools.common import caller_search_path -from .colored import ColoredFormatter, colorstr +#from .colored import ColoredFormatter, colorstr -from .obj import PpBaseObject +#from .obj import PpBaseObject from .config import CrTplConfiguration -from .handler import ExpectedHandlerError, CrTplHandler +#from .handler import ExpectedHandlerError, CrTplHandler +from .handler import CrTplHandler __version__ = '0.6.4' LOG = logging.getLogger(__name__) # ============================================================================= -class CrTplApplication(PpBaseObject): +class CrTplAppError(FbAppError): + """ Base exception class for all exceptions in this application.""" + pass + +# ============================================================================= +class CrTplApplication(BaseApplication): """ Class for the application objects. """ @@ -55,247 +62,29 @@ class CrTplApplication(PpBaseObject): initialized=False, usage=None, description=None, argparse_epilog=None, argparse_prefix_chars='-', env_prefix=None): - self.arg_parser = None - """ - @ivar: argparser object to parse commandline parameters - @type: argparse.ArgumentParser - """ - - self.args = None - """ - @ivar: an object containing all commandline parameters - after parsing them - @type: Namespace - """ - - self._exit_value = 0 - """ - @ivar: return value of the application for exiting with sys.exit(). - @type: int - """ - - self._usage = usage - """ - @ivar: usage text used on argparse - @type: str - """ - - self._description = description - """ - @ivar: a short text describing the application - @type: str - """ - - self._argparse_epilog = argparse_epilog - """ - @ivar: an epilog displayed at the end of the argparse help screen - @type: str - """ - - self._argparse_prefix_chars = argparse_prefix_chars - """ - @ivar: The set of characters that prefix optional arguments. - @type: str - """ - - self._terminal_has_colors = False - """ - @ivar: flag, that the current terminal understands color ANSI codes - @type: bool - """ - - self._quiet = False - - self.env = {} - """ - @ivar: a dictionary with all application specifiv environment variables, - they will detected by the env_prefix property of this object, - and their names will transformed before saving their values in - self.env by removing the env_prefix from the variable name. - @type: dict - """ - - self._env_prefix = None - """ - @ivar: a prefix for environment variables to detect them and to assign - their transformed names and their values in self.env - @type: str - """ + desc = textwrap.dedent("""\ + Creates in the given vSphere environment and cluster a template object, + which can be used to spawn different virtual machines. + """).strip() super(CrTplApplication, self).__init__( appname=appname, verbose=verbose, version=version, base_dir=base_dir, + description=desc, initialized=False, ) - if env_prefix: - ep = str(env_prefix).strip() - if not ep: - msg = "Invalid env_prefix {!r} given - it may not be empty.".format(env_prefix) - raise PpAppError(msg) - match = self.re_prefix.search(ep) - if not match: - msg = ( - "Invalid characters found in env_prefix {!r}, only " - "alphanumeric characters and digits and underscore " - "(this not as the first character) are allowed.").format(env_prefix) - raise PpAppError(msg) - self._env_prefix = ep - else: - ep = self.appname.upper() + '_' - self._env_prefix = self.re_anum.sub('_', ep) - - self._description = textwrap.dedent('''\ - Creates in the given vSphere environment a template object, - which can be used to spawn different virtual machines. - ''').strip() - - self._init_arg_parser() - self._perform_arg_parser() - - self._init_env() - self._perform_env() - - self.config = CrTplConfiguration( - appname=self.appname, verbose=self.verbose, base_dir=self.base_dir) - - self.handler = CrTplHandler( - appname=self.appname, verbose=self.verbose, base_dir=self.base_dir) - - self.post_init() - - # ----------------------------------------------------------- - @property - def exit_value(self): - """The return value of the application for exiting with sys.exit().""" - return self._exit_value - - @exit_value.setter - def exit_value(self, value): - v = int(value) - if v >= 0: - self._exit_value = v - else: - LOG.warn("Wrong exit_value {!r}, must be >= 0".format(value)) - - # ----------------------------------------------------------- - @property - def exitvalue(self): - """The return value of the application for exiting with sys.exit().""" - return self._exit_value - - @exitvalue.setter - def exitvalue(self, value): - self.exit_value = value - - # ----------------------------------------------------------- - @property - def usage(self): - """The usage text used on argparse.""" - return self._usage - - # ----------------------------------------------------------- - @property - def description(self): - """A short text describing the application.""" - return self._description - - # ----------------------------------------------------------- - @property - def argparse_epilog(self): - """An epilog displayed at the end of the argparse help screen.""" - return self._argparse_epilog - - # ----------------------------------------------------------- - @property - def argparse_prefix_chars(self): - """The set of characters that prefix optional arguments.""" - return self._argparse_prefix_chars - - # ----------------------------------------------------------- - @property - def terminal_has_colors(self): - """A flag, that the current terminal understands color ANSI codes.""" - return self._terminal_has_colors - - # ----------------------------------------------------------- - @property - def env_prefix(self): - """A prefix for environment variables to detect them.""" - return self._env_prefix - - # ----------------------------------------------------------- - @property - def usage_term(self): - """The localized version of 'usage: '""" - return 'Usage: ' - - # ----------------------------------------------------------- - @property - def usage_term_len(self): - """The length of the localized version of 'usage: '""" - return len(self.usage_term) - - # ----------------------------------------------------------- - @property - def quiet(self): - """Quiet execution of the application, - only warnings and errors are emitted.""" - return self._quiet - - @quiet.setter - def quiet(self, value): - self._quiet = bool(value) - - # ------------------------------------------------------------------------- - def exit(self, retval=-1, msg=None, trace=False): - """ - Universal method to call sys.exit(). If fake_exit is set, a - FakeExitError exception is raised instead (useful for unittests.) - - @param retval: the return value to give back to theoperating system - @type retval: int - @param msg: a last message, which should be emitted before exit. - @type msg: str - @param trace: flag to output a stack trace before exiting - @type trace: bool + #self.initialized = False - @return: None + #self.config = CrTplConfiguration( + # appname=self.appname, verbose=self.verbose, base_dir=self.base_dir) - """ + #self.handler = CrTplHandler( + # appname=self.appname, verbose=self.verbose, base_dir=self.base_dir) - retval = int(retval) - trace = bool(trace) - - root_logger = logging.getLogger() - has_handlers = False - if root_logger.handlers: - has_handlers = True - - if msg: - if has_handlers: - if retval: - LOG.error(msg) - else: - LOG.info(msg) - if not has_handlers: - if hasattr(sys.stderr, 'buffer'): - sys.stderr.buffer.write(str(msg) + "\n") - else: - sys.stderr.write(str(msg) + "\n") - - if trace: - if has_handlers: - if retval: - LOG.error(traceback.format_exc()) - else: - LOG.info(traceback.format_exc()) - else: - traceback.print_exc() - - sys.exit(retval) + self.initialized = True # ------------------------------------------------------------------------- def as_dict(self, short=True): @@ -310,84 +99,60 @@ class CrTplApplication(PpBaseObject): """ res = super(CrTplApplication, self).as_dict(short=short) - res['exit_value'] = self.exit_value - res['usage'] = self.usage - res['quiet'] = self.quiet - res['description'] = self.description - res['argparse_epilog'] = self.argparse_epilog - res['argparse_prefix_chars'] = self.argparse_prefix_chars - res['terminal_has_colors'] = self.terminal_has_colors - res['env_prefix'] = self.env_prefix return res - # ------------------------------------------------------------------------- - def init_logging(self): - """ - Initialize the logger object. - It creates a colored loghandler with all output to STDERR. - Maybe overridden in descendant classes. - - @return: None - """ - - log_level = logging.INFO - if self.verbose: - log_level = logging.DEBUG - elif self.quiet: - log_level = logging.WARNING - - root_logger = logging.getLogger() - root_logger.setLevel(log_level) - - # create formatter - format_str = '' - if self.verbose: - format_str = '[%(asctime)s]: ' - format_str += self.appname + ': ' - if self.verbose: - if self.verbose > 1: - format_str += '%(name)s(%(lineno)d) %(funcName)s() ' - else: - format_str += '%(name)s ' - format_str += '%(levelname)s - %(message)s' - formatter = None - if self.terminal_has_colors: - formatter = ColoredFormatter(format_str) - else: - formatter = logging.Formatter(format_str) - - # create log handler for console output - lh_console = logging.StreamHandler(sys.stderr) - lh_console.setLevel(log_level) - lh_console.setFormatter(formatter) - - root_logger.addHandler(lh_console) - - if self.verbose < 3: - paramiko_logger = logging.getLogger('paramiko.transport') - if self.verbose < 1: - paramiko_logger.setLevel(logging.WARNING) - else: - paramiko_logger.setLevel(logging.INFO) - - return - - # ------------------------------------------------------------------------- - def terminal_can_color(self): - """ - Method to detect, whether the current terminal (stdout and stderr) - is able to perform ANSI color sequences. - - @return: both stdout and stderr can perform ANSI color sequences - @rtype: bool - - """ - - term_debug = False - if self.verbose > 3: - term_debug = True - return terminal_can_colors(debug=term_debug) +# # ------------------------------------------------------------------------- +# def init_logging(self): +# """ +# Initialize the logger object. +# It creates a colored loghandler with all output to STDERR. +# Maybe overridden in descendant classes. +# +# @return: None +# """ +# +# log_level = logging.INFO +# if self.verbose: +# log_level = logging.DEBUG +# elif self.quiet: +# log_level = logging.WARNING +# +# root_logger = logging.getLogger() +# root_logger.setLevel(log_level) +# +# # create formatter +# format_str = '' +# if self.verbose: +# format_str = '[%(asctime)s]: ' +# format_str += self.appname + ': ' +# if self.verbose: +# if self.verbose > 1: +# format_str += '%(name)s(%(lineno)d) %(funcName)s() ' +# else: +# format_str += '%(name)s ' +# format_str += '%(levelname)s - %(message)s' +# formatter = None +# if self.terminal_has_colors: +# formatter = ColoredFormatter(format_str) +# else: +# formatter = logging.Formatter(format_str) +# +# # create log handler for console output +# lh_console = logging.StreamHandler(sys.stderr) +# lh_console.setLevel(log_level) +# lh_console.setFormatter(formatter) +# +# root_logger.addHandler(lh_console) +# +# if self.verbose < 3: +# paramiko_logger = logging.getLogger('paramiko.transport') +# if self.verbose < 1: +# paramiko_logger.setLevel(logging.WARNING) +# else: +# paramiko_logger.setLevel(logging.INFO) +# +# return # ------------------------------------------------------------------------- def post_init(self): @@ -403,203 +168,34 @@ class CrTplApplication(PpBaseObject): """ self.init_logging() - self.config.read() - if self.config.verbose > self.verbose: - self.verbose = self.config.verbose +# self.config.read() +# if self.config.verbose > self.verbose: +# self.verbose = self.config.verbose self.perform_arg_parser() - if not self.config.password: - prompt = 'Enter password for host {h!r} and user {u!r}: '.format( - h=self.config.vsphere_host, u=self.config.vsphere_user) - self.config.password = getpass.getpass(prompt=prompt) - - self.handler.config = self.config - if self.args.rotate: - self.handler.rotate_only = True - if self.args.abort: - self.handler.abort = True - - self.handler.initialized = True - self.initialized = True - - # ------------------------------------------------------------------------- - def pre_run(self): - """ - Dummy function to run before the main routine. - Could be overwritten by descendant classes. - - """ - - pass - - # ------------------------------------------------------------------------- - def _run(self): - """ - Dummy function as main routine. - - MUST be overwritten by descendant classes. - - """ - - LOG.info("Starting {a!r}, version {v!r} ...".format( - a=self.appname, v=self.version)) - - try: - ret = self.handler() - self.exit(ret) - except ExpectedHandlerError as e: - self.handle_error(str(e), "Temporary VM") - self.exit(5) - - # ------------------------------------------------------------------------- - def __call__(self): - """ - Helper method to make the resulting object callable, e.g.:: - - app = PBApplication(...) - app() - - @return: None - - """ - - self.run() - - # ------------------------------------------------------------------------- - def run(self): - """ - The visible start point of this object. - - @return: None - - """ - - if not self.initialized: - self.handle_error( - "The application is not completely initialized.", '', True) - self.exit(9) - - try: - self.pre_run() - except Exception as e: - self.handle_error(str(e), e.__class__.__name__, True) - self.exit(98) - - if not self.initialized: - raise PpAppError( - "Object {!r} seems not to be completely initialized.".format( - self.__class__.__name__)) - - try: - self._run() - except Exception as e: - self.handle_error(str(e), e.__class__.__name__, True) - self.exit_value = 99 - - if self.verbose > 1: - LOG.info("Ending.") +# if not self.config.password: +# prompt = 'Enter password for host {h!r} and user {u!r}: '.format( +# h=self.config.vsphere_host, u=self.config.vsphere_user) +# self.config.password = getpass.getpass(prompt=prompt) - try: - self.post_run() - except Exception as e: - self.handle_error(str(e), e.__class__.__name__, True) - self.exit_value = 97 - - self.exit(self.exit_value) - - # ------------------------------------------------------------------------- - def post_run(self): - """ - Dummy function to run after the main routine. - Could be overwritten by descendant classes. - - """ +# self.handler.config = self.config +# if self.args.rotate: +# self.handler.rotate_only = True +# if self.args.abort: +# self.handler.abort = True - if self.verbose > 1: - LOG.info("executing post_run() ...") - - # ------------------------------------------------------------------------- - def _init_arg_parser(self): - """ - Local called method to initiate the argument parser. - - @raise PBApplicationError: on some errors - - """ - - self.arg_parser = argparse.ArgumentParser( - prog=self.appname, - description=self.description, - usage=self.usage, - epilog=self.argparse_epilog, - prefix_chars=self.argparse_prefix_chars, - add_help=False, - ) - - self.init_arg_parser() - - general_group = self.arg_parser.add_argument_group('General options') - general_group.add_argument( - '--color', - action="store", - dest='color', - const='yes', - default='auto', - nargs='?', - choices=['yes', 'no', 'auto'], - help="Use colored output for messages.", - ) - - verbose_group = general_group.add_mutually_exclusive_group() - - verbose_group.add_argument( - "-v", "--verbose", - action="count", - dest='verbose', - help='Increase the verbosity level', - ) - - verbose_group.add_argument( - "-q", "--quiet", - action="store_true", - dest='quiet', - help='Silent execution, only warnings and errors are emitted.', - ) - - general_group.add_argument( - "-h", "--help", - action='help', - dest='help', - help='Show this help message and exit' - ) - general_group.add_argument( - "--usage", - action='store_true', - dest='usage', - help="Display brief usage message and exit" - ) - general_group.add_argument( - "-V", '--version', - action='version', - version='Version of %(prog)s: {}'.format(self.version), - help="Show program's version number and exit" - ) +# self.handler.initialized = True +# self.initialized = True # ------------------------------------------------------------------------- def init_arg_parser(self): """ Public available method to initiate the argument parser. - - Note:: - avoid adding the general options '--verbose', '--help', '--usage' - and '--version'. These options are allways added after executing - this method. - - Descendant classes may override this method. - """ + super(CrTplApplication, self).init_arg_parser() + self.arg_parser.add_argument( '-A', '--abort', dest='abort', action='store_true', help="Abort creation of VMWare template after successsful creation of template VM.", @@ -664,210 +260,62 @@ class CrTplApplication(PpBaseObject): help="Execute rortation of existing templates only, don't create a new one." ) - # ------------------------------------------------------------------------- - def _perform_arg_parser(self): - """ - Underlaying method for parsing arguments. - """ - - self.args = self.arg_parser.parse_args() - - if self.args.usage: - self.arg_parser.print_usage(sys.stdout) - self.exit(0) - - if self.args.verbose is not None and self.args.verbose > self.verbose: - self.verbose = self.args.verbose - - if self.args.quiet: - self.quiet = self.args.quiet - - if self.args.color == 'yes': - self._terminal_has_colors = True - elif self.args.color == 'no': - self._terminal_has_colors = False - else: - self._terminal_has_colors = self.terminal_can_color() - # ------------------------------------------------------------------------- def perform_arg_parser(self): """ Public available method to execute some actions after parsing the command line parameters. - - Descendant classes may override this method. - """ - - if self.args.host: - self.config.vsphere_host = self.args.host - if self.args.port: - self.config.vsphere_port = self.args.port - if self.args.user: - self.config.vsphere_user = self.args.user - if self.args.password: - self.config.password = self.args.password - if self.args.folder: - self.config.folder = self.args.folder - if self.args.vm: - self.config.template_vm = self.args.vm - if self.args.template: - self.config.template_name = self.args.template - - if self.args.number is not None: - v = self.args.number - if v < 1: - LOG.error(( - "Wrong number {} of templates to stay in templates folder, " - "must be greater than zero.").format(v)) - elif v >= 100: - LOG.error(( - "Wrong number {} of templates to stay in templates folder, " - "must be less than 100.").format(v)) - else: - self.config.max_nr_templates_stay = v - - # ------------------------------------------------------------------------- - def _init_env(self): - """ - Initialization of self.env by application specific environment - variables. - - It calls self.init_env(), after it has done his job. - - """ - - for (key, value) in list(os.environ.items()): - - if not key.startswith(self.env_prefix): - continue - - newkey = key.replace(self.env_prefix, '', 1) - self.env[newkey] = value - - self.init_env() - - # ------------------------------------------------------------------------- - def init_env(self): - """ - Public available method to initiate self.env additional to the implicit - initialization done by this module. - Maybe it can be used to import environment variables, their - names not starting with self.env_prefix. - - Currently a dummy method, which ca be overriden by descendant classes. - """ pass - # ------------------------------------------------------------------------- - def _perform_env(self): - """ - Method to do some useful things with the found environment. - - It calls self.perform_env(), after it has done his job. - - """ - - # try to detect verbosity level from environment - if 'VERBOSE' in self.env and self.env['VERBOSE']: - v = 0 - try: - v = int(self.env['VERBOSE']) - except ValueError: - v = 1 - if v > self.verbose: - self.verbose = v - - self.perform_env() +# if self.args.host: +# self.config.vsphere_host = self.args.host +# if self.args.port: +# self.config.vsphere_port = self.args.port +# if self.args.user: +# self.config.vsphere_user = self.args.user +# if self.args.password: +# self.config.password = self.args.password +# if self.args.folder: +# self.config.folder = self.args.folder +# if self.args.vm: +# self.config.template_vm = self.args.vm +# if self.args.template: +# self.config.template_name = self.args.template +# +# if self.args.number is not None: +# v = self.args.number +# if v < 1: +# LOG.error(( +# "Wrong number {} of templates to stay in templates folder, " +# "must be greater than zero.").format(v)) +# elif v >= 100: +# LOG.error(( +# "Wrong number {} of templates to stay in templates folder, " +# "must be less than 100.").format(v)) +# else: +# self.config.max_nr_templates_stay = v # ------------------------------------------------------------------------- - def perform_env(self): - """ - Public available method to perform found environment variables after - initialization of self.env. - - Currently a dummy method, which ca be overriden by descendant classes. - - """ - - pass - - # ------------------------------------------------------------------------- - def colored(self, msg, color): - """ - Wrapper function to colorize the message. Depending, whether the current - terminal can display ANSI colors, the message is colorized or not. - - @param msg: The message to colorize - @type msg: str - @param color: The color to use, must be one of the keys of COLOR_CODE - @type color: str - - @return: the colorized message - @rtype: str - + def _run(self): """ + Dummy function as main routine. - if not self.terminal_has_colors: - return msg - return colorstr(msg, color) + MUST be overwritten by descendant classes. - # ------------------------------------------------------------------------- - def get_command(self, cmd, quiet=False): """ - Searches the OS search path for the given command and gives back the - normalized position of this command. - If the command is given as an absolute path, it check the existence - of this command. - - @param cmd: the command to search - @type cmd: str - @param quiet: No warning message, if the command could not be found, - only a debug message - @type quiet: bool - @return: normalized complete path of this command, or None, - if not found - @rtype: str or None + LOG.info("Starting {a!r}, version {v!r} ...".format( + a=self.appname, v=self.version)) - """ +# try: +# ret = self.handler() +# self.exit(ret) +# except ExpectedHandlerError as e: +# self.handle_error(str(e), "Temporary VM") +# self.exit(5) - if self.verbose > 2: - LOG.debug("Searching for command {!r} ...".format(cmd)) - - # Checking an absolute path - if os.path.isabs(cmd): - if not os.path.exists(cmd): - LOG.warning("Command {!r} doesn't exists.".format(cmd)) - return None - if not os.access(cmd, os.X_OK): - msg = "Command {!r} is not executable.".format(cmd) - LOG.warning(msg) - return None - return os.path.normpath(cmd) - - # Checking a relative path - for d in caller_search_path(): - if self.verbose > 3: - LOG.debug("Searching command in {!r} ...".format(d)) - p = os.path.join(d, cmd) - if os.path.exists(p): - if self.verbose > 2: - LOG.debug("Found {!r} ...".format(p)) - if os.access(p, os.X_OK): - return os.path.normpath(p) - else: - LOG.debug("Command {!r} is not executable.".format(p)) - - # command not found, sorry - if quiet: - if self.verbose > 2: - LOG.debug("Command {!r} not found.".format(cmd)) - else: - LOG.warning("Command {!r} not found.".format(cmd)) - - return None # ============================================================================= diff --git a/python_fb_tools b/python_fb_tools index ce5a2de..9e480d2 160000 --- a/python_fb_tools +++ b/python_fb_tools @@ -1 +1 @@ -Subproject commit ce5a2de8a61ce56893a2c9fcee19a7ff750daa68 +Subproject commit 9e480d2af8e47fac04e349dcde092a036f583134 diff --git a/requirements.txt b/requirements.txt index ec58427..5556836 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ six pytz paramiko flake8 +pathlib -- 2.39.5