From 3f5339352f20b933569f3ab0d719520a6f2d5975 Mon Sep 17 00:00:00 2001 From: Luc Duron Date: Fri, 3 Nov 2017 09:08:24 +0100 Subject: [PATCH] [cli] Add initial comand line scripts --- Makefile | 3 +- PyTelTools/__init__.py | 0 PyTelTools/slf/Serafin.py | 51 ++++++----- PyTelTools/slf/datatypes.py | 7 +- PyTelTools/slf/misc.py | 5 +- PyTelTools/slf/util.py | 10 +++ PyTelTools/slf/variable/variables_utils.py | 5 +- PyTelTools/slf/variables.py | 1 + cli/slf_base.py | 92 ++++++++++++++++++++ cli/slf_last.py | 61 ++++++++++++++ cli/utils/__init__.py | 0 cli/utils/util.py | 98 ++++++++++++++++++++++ cli/write_cli_usage.py | 46 ++++++++++ conventions.md | 5 ++ requirements.txt | 1 + setup.py | 9 +- 16 files changed, 363 insertions(+), 31 deletions(-) create mode 100644 PyTelTools/__init__.py create mode 100644 PyTelTools/slf/util.py create mode 100755 cli/slf_base.py create mode 100755 cli/slf_last.py create mode 100644 cli/utils/__init__.py create mode 100644 cli/utils/util.py create mode 100755 cli/write_cli_usage.py diff --git a/Makefile b/Makefile index 7be95cd..24d2cff 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ DOC_PATH=../CNR-Engineering.github.io/PyTelTools doc: doxygen.config doxygen $< + export PYTHONPATH=$(shell pwd) && cd cli && python write_cli_usage.py update_doc: doc rm -r ${DOC_PATH} && cp -r doc/html/* ${DOC_PATH} @@ -37,4 +38,4 @@ test: clean: rm -rf doc venv .cache -.PHONY: clean test update_doc venv +.PHONY: clean doc test update_doc venv diff --git a/PyTelTools/__init__.py b/PyTelTools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PyTelTools/slf/Serafin.py b/PyTelTools/slf/Serafin.py index 0d1e265..c3013c2 100755 --- a/PyTelTools/slf/Serafin.py +++ b/PyTelTools/slf/Serafin.py @@ -9,11 +9,11 @@ """ import copy -import logging import numpy as np import os import struct +from slf.util import logger from slf.variable.variables_2d import VARIABLES_2D from slf.variable.variables_3d import VARIABLES_3D @@ -22,9 +22,6 @@ SLF_EIT = 'iso-8859-1' -module_logger = logging.getLogger(__name__) - - VARIABLES_ID_2D, VARIABLES_ID_3D = {'fr': {}, 'en': {}}, {'fr': {}, 'en': {}} @@ -51,7 +48,7 @@ def __init__(self, message): """ super().__init__(message) self.message = message - module_logger.error('SERAFIN VALIDATION ERROR: %s' % message) + logger.error('SERAFIN VALIDATION ERROR: %s' % message) class SerafinRequestError(Exception): @@ -64,7 +61,7 @@ def __init__(self, message): """ super().__init__(message) self.message = message - module_logger.error('SERAFIN REQUEST ERROR: %s' % message) + logger.error('SERAFIN REQUEST ERROR: %s' % message) class SerafinHeader: @@ -96,7 +93,7 @@ def __init__(self, file, file_size, language): self.endian = '<' else: raise SerafinValidationError('File endianness could not be determined') - module_logger.debug('File is determined to be %s endian' % ('big' if self.endian == '>' else 'litte')) + logger.debug('File is determined to be %s endian' % ('big' if self.endian == '>' else 'litte')) # Read title self.title = file.read(72) @@ -105,7 +102,7 @@ def __init__(self, file, file_size, language): try: data_format = self.file_type.decode(SLF_EIT) - module_logger.debug('The file type is: "%s"' % data_format) + logger.debug('The file type is: "%s"' % data_format) except UnicodeDecodeError: raise SerafinValidationError('File type is unreadable: %s' % self.file_type) @@ -113,7 +110,7 @@ def __init__(self, file, file_size, language): self._set_as_double_precision() else: if data_format not in ('SERAFIN ', 'SERAPHIN', ' '): - module_logger.warning('Format "%s" is unknown and is forced to "SERAFIN"' % data_format) + logger.warning('Format "%s" is unknown and is forced to "SERAFIN"' % data_format) self.file_type = bytes('SERAFIN', SLF_EIT).ljust(8) self._set_as_single_precision() @@ -121,7 +118,7 @@ def __init__(self, file, file_size, language): file.read(4) self.nb_var = self.unpack_int(file.read(4), 1)[0] self.nb_var_quadratic = self.unpack_int(file.read(4), 1)[0] - module_logger.debug('The file has %d variables' % self.nb_var) + logger.debug('The file has %d variables' % self.nb_var) file.read(4) if self.nb_var_quadratic != 0: raise SerafinValidationError('The number of quadratic variables is not equal to zero') @@ -171,7 +168,7 @@ def __init__(self, file, file_size, language): raise SerafinValidationError('The number of nodes per element is not equal to 6') if self.nb_planes < 2: raise SerafinValidationError('The number of planes is less than 2') - module_logger.debug('The file is determined to be %s' % {True: '2D', False: '3D'}[self.is_2d]) + logger.debug('The file is determined to be %s' % {True: '2D', False: '3D'}[self.is_2d]) # determine the number of nodes in 2D if self.is_2d: @@ -207,7 +204,7 @@ def __init__(self, file, file_size, language): # Deduce the number of frames and test the integer division self.nb_frames = (self.file_size - self.header_size) // self.frame_size - module_logger.debug('The file has %d frames of size %d bytes' % (self.nb_frames, self.frame_size)) + logger.debug('The file has %d frames of size %d bytes' % (self.nb_frames, self.frame_size)) diff_size = self.file_size - self.header_size - self.frame_size * self.nb_frames # A difference of only one byte is tolerated. @@ -222,7 +219,7 @@ def __init__(self, file, file_size, language): name = var_name.decode(encoding=SLF_EIT).strip() if name not in var_table: slf_type = '2D' if self.is_2d else '3D' - module_logger.warning('WARNING: The %s variable name "%s" is not known (lang=%s). ' + logger.warning('WARNING: The %s variable name "%s" is not known (lang=%s). ' 'The complete name will be used as ID' % (slf_type, name, self.language)) var_id = name else: @@ -242,7 +239,7 @@ def __init__(self, file, file_size, language): else: self.ikle_2d = self.ikle.reshape(self.nb_elements, self.nb_nodes_per_elem) - module_logger.debug('Finished reading the header') + logger.debug('Finished reading the header') def _set_as_single_precision(self): """Set Serafin as single precision""" @@ -296,6 +293,14 @@ def pack_float(self, *args, nb=1): fmt = self.endian + str(nb) + self.float_type return struct.pack(fmt, *args) + def toggle_endianness(self): + """Toggle original endianness (between big or little endian)""" + if self.endian == '>': + self.endian = '<' + else: + self.endian = '>' + logger.debug('Toggle endianness to %s' % ('big' if self.endian == '>' else 'litte')) + def _set_header_size(self): """Set header size""" nb_ikle_values = self.nb_elements * self.nb_nodes_per_elem @@ -484,7 +489,7 @@ def __init__(self, filename, language): self.header = None self.time = [] self.file_size = os.path.getsize(self.filename) - module_logger.info('Reading the input file: "%s" of size %d bytes' % (filename, self.file_size)) + logger.info('Reading the input file: "%s" of size %d bytes' % (filename, self.file_size)) def read_header(self): """! @@ -498,7 +503,7 @@ def get_time(self): """ if self.header is None: raise SerafinRequestError('Cannot read time without any header (forgot read_header ?)') - module_logger.debug('Reading the time series from the file') + logger.debug('Reading the time series from the file') self.file.seek(self.header.header_size, 0) for i in range(self.header.nb_frames): self.file.read(4) @@ -506,6 +511,14 @@ def get_time(self): self.file.read(4) self.file.seek(self.header.frame_size - 8 - self.header.float_size, 1) + def subset_time(self, start, end, ech): + new_time = [] + for time_index, time in enumerate(self.time): + if start <= time <= end: + if time_index % ech == 0: + new_time.append((time_index, time)) + return new_time + def _get_var_index(self, var_ID): """! @brief Handle data request by variable ID @@ -527,7 +540,7 @@ def read_var_in_frame(self, time_index, var_ID): @param var_ID : variable ID @return : values of the variables, of length equal to the number of nodes """ - module_logger.debug('Reading variable %s at frame %i' % (var_ID, time_index)) + logger.debug('Reading variable %s at frame %i' % (var_ID, time_index)) pos_var = self._get_var_index(var_ID) self.file.seek(self.header.header_size + time_index * self.header.frame_size + 8 + self.header.float_size + pos_var * (8 + self.header.float_size * self.header.nb_nodes), 0) @@ -568,13 +581,13 @@ class Write(Serafin): """ def __init__(self, filename, language): super().__init__(filename, 'wb', language) - module_logger.info('Writing the output file: "%s"' % filename) + logger.info('Writing the output file: "%s"' % filename) def __enter__(self): try: return Serafin.__enter__(self) except FileExistsError: - module_logger.error('ERROR: Cannot overwrite existing file') + logger.error('ERROR: Cannot overwrite existing file') raise FileExistsError('File {} already exists (remove the file or change the option ' 'and then re-run the program)'.format(self.filename)) diff --git a/PyTelTools/slf/datatypes.py b/PyTelTools/slf/datatypes.py index 1421b9a..20ada7d 100755 --- a/PyTelTools/slf/datatypes.py +++ b/PyTelTools/slf/datatypes.py @@ -1,11 +1,8 @@ from copy import deepcopy import datetime -import logging -import numpy as np from slf import Serafin - -module_logger = logging.getLogger(__name__) +from slf.util import logger class SerafinData: @@ -43,7 +40,7 @@ def read(self): year, month, day, hour, minute, second = self.header.date self.start_time = datetime.datetime(year, month, day, hour, minute, second) except ValueError: - module_logger.warning('Date seems invalid, replaced by default date.') + logger.warning('Date seems invalid, replaced by default date.') if self.start_time is None: self.start_time = datetime.datetime(1900, 1, 1, 0, 0, 0) self.time_second = list(map(lambda x: datetime.timedelta(seconds=x), self.time)) diff --git a/PyTelTools/slf/misc.py b/PyTelTools/slf/misc.py index d822ae8..d4f3dd4 100755 --- a/PyTelTools/slf/misc.py +++ b/PyTelTools/slf/misc.py @@ -8,11 +8,10 @@ import shapefile from slf import Serafin +from slf.util import logger from slf.variables import do_calculation, get_available_variables, get_necessary_equations -module_logger = logging.getLogger(__name__) - # constants OPERATORS = ['+', '-', '*', '/', '^', 'sqrt', 'sin', 'cos', 'atan'] MAX, MIN, MEAN, ARRIVAL_DURATION, PROJECT, DIFF, REV_DIFF, \ @@ -67,7 +66,7 @@ def scalars_vectors(known_vars, selected_vars, us_equation=None): is_2d=True, us_equation=us_equation)) continue # if the magnitude is not computable, use scalar operation instead - module_logger.warning('The variable %s will be considered to be scalar instead of vector.' % var) + logger.warning('The variable %s will be considered to be scalar instead of vector.' % var) scalars.append((var, name, unit)) else: scalars.append((var, name, unit)) diff --git a/PyTelTools/slf/util.py b/PyTelTools/slf/util.py new file mode 100644 index 0000000..1802f66 --- /dev/null +++ b/PyTelTools/slf/util.py @@ -0,0 +1,10 @@ +import logging + +from conf.settings import LOGGING_LEVEL + + +logger = logging.getLogger(__name__) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter('%(message)s')) +logger.addHandler(handler) +logger.setLevel(LOGGING_LEVEL) diff --git a/PyTelTools/slf/variable/variables_utils.py b/PyTelTools/slf/variable/variables_utils.py index da3cdff..ca3aebc 100755 --- a/PyTelTools/slf/variable/variables_utils.py +++ b/PyTelTools/slf/variable/variables_utils.py @@ -4,6 +4,7 @@ import numpy as np + # define constants KARMAN = 0.4 RHO_WATER = 1000. @@ -84,8 +85,8 @@ def compute_COMPONENT_X(scalar, x, y): return np.where(magnitude>0, scalar*x/magnitude, 0) -def compute_COMPONENT_Y(a, x, y): - return compute_COMPONENT_X(a, y, x) +def compute_COMPONENT_Y(scalar, x, y): + return compute_COMPONENT_X(scalar, y, x) PLUS, MINUS, TIMES, NORM2, NORM2_3D = 1, 2, 3, 4, 104 diff --git a/PyTelTools/slf/variables.py b/PyTelTools/slf/variables.py index 4c4b3c0..0695aa6 100644 --- a/PyTelTools/slf/variables.py +++ b/PyTelTools/slf/variables.py @@ -6,6 +6,7 @@ from slf.variable.variables_2d import get_available_2d_variables, get_necessary_2d_equations, \ get_US_equation, new_variables_from_US +# Beware: `get_US_equation` and `new_variables_from_US` are imported indirectly from slf.variable.variables_3d import get_available_3d_variables, get_necessary_3d_equations from slf.variable.variables_utils import do_calculation diff --git a/cli/slf_base.py b/cli/slf_base.py new file mode 100755 index 0000000..b4e0476 --- /dev/null +++ b/cli/slf_base.py @@ -0,0 +1,92 @@ +#!/usr/bin/python3 +""" +Performs multiple operations on a Serafin file: +- compute and/or remove variables +- mesh transformations (coordinates) +- select frames +""" + +import sys +from tqdm import tqdm + +from utils.util import logger, MyArgParse +from PyTelTools.geom.transformation import Translation +from PyTelTools.slf import Serafin +from PyTelTools.slf.variables import do_calculations_in_frame, get_necessary_equations + + +def slf_base(args): + with Serafin.Read(args.in_slf, args.lang) as resin: + resin.read_header() + logger.info(resin.header.summary()) + resin.get_time() + + output_header = resin.header.copy() + # Shift mesh coordinates if necessary + if args.shift: + output_header.transform_mesh(Translation(args.shift[0], args.shift[1], 0)) + + # Toogle output file endianness if necessary + if args.toggle_endianness: + output_header.toggle_endianness() + + # Remove variables if necessary + output_header.empty_variables() + for var_ID, var_name, var_unit in zip(resin.header.var_IDs, resin.header.var_names, resin.header.var_units): + if args.var2del is not None: + if var_ID not in args.var2del: + output_header.add_variable(var_ID, var_name, var_unit) + else: + output_header.add_variable(var_ID, var_name, var_unit) + + # Add new derived variables + if args.var2add is not None: + for var_ID in args.var2add: + if var_ID in output_header.var_IDs: + logger.warn('Variable %s is already present (or asked)' % var_ID) + else: + output_header.add_variable_from_ID(var_ID) + + # Convert to single precision + if args.to_single_precision: + if resin.header.is_double_precision(): + output_header.to_single_precision() + else: + logger.warn('Input file is already single precision! Argument `--to_single_precision` is ignored') + + necessary_equations = get_necessary_equations(resin.header.var_IDs, output_header.var_IDs, + is_2d=resin.header.is_2d) + + with Serafin.Write(args.out_slf, args.lang) as resout: + resout.write_header(resin.header) + + for time_index, time in tqdm(resin.subset_time(args.start, args.end, args.ech), unit='frame'): + values = do_calculations_in_frame(necessary_equations, resin, time_index, output_header.var_IDs, + output_header.np_float_type, is_2d=output_header.is_2d, + us_equation=None) + resout.write_entire_frame(output_header, time, values) + + +parser = MyArgParse(description=__doc__, add_args=['in_slf', 'out_slf', 'shift']) + +group_var = parser.add_argument_group('Serafin variables (optional)', + 'See variables abbrevations on https://github.com/CNR-Engineering/PyTelTools/wiki/Notations-of-variables') +group_var.add_argument('--var2del', nargs='+', help='variable(s) to delete', default=[], metavar=('V1', 'V2')) +group_var.add_argument('--var2add', nargs='+', help='variable(s) to add', default=[], metavar=('V1', 'V2')) + +group_temp = parser.add_argument_group('Temporal operations (optional)') +group_temp.add_argument('--ech', type=int, help='frequency sampling of input', default=1) +group_temp.add_argument('--start', type=float, help='minimum time (in seconds)', default=-float('inf')) +group_temp.add_argument('--end', type=float, help='maximum time (in seconds)', default=float('inf')) + +parser.add_group_general(['force', 'verbose']) + + +if __name__ == '__main__': + args = parser.parse_args() + + try: + slf_base(args) + except (Serafin.SerafinRequestError, Serafin.SerafinValidationError): + # Message is already reported by slf logger + sys.exit(2) diff --git a/cli/slf_last.py b/cli/slf_last.py new file mode 100755 index 0000000..33fa8f9 --- /dev/null +++ b/cli/slf_last.py @@ -0,0 +1,61 @@ +#!/usr/bin/python3 +""" +Extract last temporal frame of a 2D/3D Serafin file +""" + +import numpy as np +import sys + +from utils.util import logger, MyArgParse +from PyTelTools.geom.transformation import Translation +from PyTelTools.slf import Serafin + + +def slf_last(args): + with Serafin.Read(args.in_slf, args.lang) as resin: + resin.read_header() + logger.info(resin.header.summary()) + resin.get_time() + + output_header = resin.header.copy() + # Shift mesh coordinates if necessary + if args.shift: + output_header.transform_mesh(Translation(args.shift[0], args.shift[1], 0)) + + # Toogle output file endianness if necessary + if args.toggle_endianness: + output_header.toggle_endianness() + + # Convert to single precision + if args.to_single_precision: + if resin.header.is_double_precision(): + output_header.to_single_precision() + else: + logger.warn('Input file is already single precision! Argument `--to_single_precision` is ignored') + + values = np.empty((output_header.nb_var, resin.header.nb_nodes), dtype=resin.header.np_float_type) + with Serafin.Write(args.out_slf, args.lang) as resout: + resout.write_header(resin.header) + + time_index = -1 + time = resin.time[-1] if args.time is None else args.time + + for i, var_ID in enumerate(resin.header.var_IDs): + values[i, :] = resin.read_var_in_frame(time_index, var_ID) + + resout.write_entire_frame(output_header, time, values) + + +parser = MyArgParse(description=__doc__, add_args=['in_slf', 'out_slf', 'shift']) +parser.add_argument('--time', help='time in seconds to write last frame (set to frame time by default)', type=float) +parser.add_group_general(['force', 'verbose']) + + +if __name__ == '__main__': + args = parser.parse_args() + + try: + slf_last(args) + except (Serafin.SerafinRequestError, Serafin.SerafinValidationError): + # Message is already reported by slf logger + sys.exit(2) diff --git a/cli/utils/__init__.py b/cli/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/utils/util.py b/cli/utils/util.py new file mode 100644 index 0000000..724bd11 --- /dev/null +++ b/cli/utils/util.py @@ -0,0 +1,98 @@ +""" +Prepare module logger + +Handles some exceptions (if `in_slf` or `out_slf` arguments are present) +""" +import argparse +import logging +import sys + +from PyTelTools.conf.settings import LANG, LOGGING_LEVEL +from PyTelTools.slf.Serafin import logger as slf_logger + + +LINE_WIDTH = 80 + +logger = logging.getLogger(__name__) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter('%(message)s')) +logger.addHandler(handler) +logger.setLevel(LOGGING_LEVEL) + + +class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): + pass + + +class MyArgParse(argparse.ArgumentParser): + """ + Derived ArgumentParser with improved help message rendering + """ + def __init__(self, add_args=[], description=None, *args, **kwargs): + kwargs['formatter_class'] = CustomFormatter + new_description = '_' * LINE_WIDTH + '\n' + description + '_' * LINE_WIDTH + '\n' + super().__init__(add_help=False, description=new_description, *args, **kwargs) + self._positionals.title = self._title_group('Positional and compulsory arguments') + self._optionals.title = self._title_group('Optional arguments') + self.group_general = None + + if 'in_slf' in add_args: + self.add_argument('in_slf', help='Serafin input filename') + if 'out_slf' in add_args: + self.add_argument('out_slf', help='Serafin output filename') + self.add_argument('--to_single_precision', help='force Serafin output to be single precision', + action='store_true') + self.add_argument('--toggle_endianness', help='toggle output file endianness (between big/little endian)', + action='store_true') + if any(arg in add_args for arg in ('in_slf', 'out_slf')): + self.add_argument('--lang', help='Serafin language for variables detection (`fr` or `en`)', + default=LANG) + if 'shift' in add_args: + self.add_argument('--shift', type=float, nargs=2, help='translation (x_distance, y_distance)', + metavar=('X', 'Y')) + + @staticmethod + def _title_group(label): + """Decorates group title label""" + return '~> '+ label + + def add_argument_group(self, name, *args, **kwargs): + """Add title group decoration""" + return super().add_argument_group(self._title_group(name), *args, **kwargs) + + def add_group_general(self, add_args): + """Add group for optional general arguments (commonly used in PyTelTools)""" + self.group_general = self.add_argument_group('General optional arguments') + if 'force' in add_args: + self.group_general.add_argument('-f' '--force', help='force output overwrite', action='store_true') + if 'verbose' in add_args: + self.group_general.add_argument('-v', '--verbose', help='increase output verbosity', action='store_true') + self.group_general.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, + help='show this help message and exit') + + def parse_args(self, *args, **kwargs): + new_args = super().parse_args(*args, **kwargs) + + if self.group_general is None: + self.add_group_general() # add only help message + + # Set module logger verbosity + if 'verbose' in new_args: + if new_args.verbose: + logger.setLevel(logging.DEBUG) + slf_logger.setLevel(logging.DEBUG) + + # Input Serafin file + if 'in_slf' in new_args: + try: + with open(new_args.in_slf): + pass + except FileNotFoundError: + logger.error('No such file or directory: %s' % new_args.in_slf) + sys.exit(1) + + if any(arg in new_args for arg in ('in_slf', 'out_slf')): + if 'slf_lang' not in new_args: + new_args.in_lang = LANG + + return new_args diff --git a/cli/write_cli_usage.py b/cli/write_cli_usage.py new file mode 100755 index 0000000..0299310 --- /dev/null +++ b/cli/write_cli_usage.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 +""" +Write a markdown documentation file for command line scripts. + +/!\ Should be run from PyTelTools repository root folder (same level as Makefile). +""" + +import importlib +import os.path +from glob import glob + + +class CommandLineScript: + def __init__(self, path): + self.path = path + basename = os.path.basename(self.path) + self.name = os.path.splitext(basename)[0] + + def help_msg(self): + """Returns help message with description and usage""" + mod = importlib.import_module(self.name) + return getattr(mod, 'parser').format_help() + + +# Build sorted list of CLI scripts +cli_scripts = [] +for file in sorted(glob('*.py')): + if not file.endswith('__init__.py'): + cli_scripts.append(CommandLineScript(file)) + + +# Write a markdown file (to be integrated within github wiki) +with open(os.path.join('..', 'doc', 'cli_usage.md'), 'w') as fileout: + # Write TOC + for script in cli_scripts: + fileout.write('* [%s](#%s)\n' % (script.name, script.name)) + fileout.write('\n') + + # Write help message for each script + for script in cli_scripts: + print(script.name) + fileout.write('# %s\n' % script.name) + fileout.write('```\n') + fileout.write(script.help_msg()) + fileout.write('```\n') + fileout.write('\n') diff --git a/conventions.md b/conventions.md index bb0f1d1..fb3a972 100755 --- a/conventions.md +++ b/conventions.md @@ -56,6 +56,11 @@ Use with following logging levels (with corresponding numeric value) : * INFO (20) * DEBUG (10) +## CLI exiting code +* 0 = successful termination +* 1 = error with command-line arguments +* 2 = error while parsing file + ## Code documentation Developper documentation is generated with doyxgen and provided on https://cnr-engineering.github.io/PyTelTools. diff --git a/requirements.txt b/requirements.txt index 00195de..0c813ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ Rtree shapely +tqdm numpy scipy matplotlib diff --git a/setup.py b/setup.py index cd8027b..f53713e 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,25 @@ #!/usr/bin/env python +from glob import glob from setuptools import setup with open('requirements.txt') as f: requirements = f.read().splitlines() +cli_files = [] +for file in glob('cli/*.py'): + if not file.endswith('__init__.py'): + cli_files.append(file) + setup( name='PyTelTools', - version='0.2', + version='0.21', author='Luc Duron', author_email='l.duron@cnr.tm.fr', py_modules=['PyTelTools'], install_requires=requirements, description='Python library for Telemac post-processing tasks', url='https://github.com/CNR-Engineering/PyTelTools', + scripts=cli_files, )