Skip to content
This repository was archived by the owner on Jul 12, 2022. It is now read-only.

Commit 6ba0e7a

Browse files
committed
main: Fixes and enhancements for main and model.
* main: ** Runs from cmd-line! ** Improve logging in DEBUG and enable -verbose flag to print sensible constructed model. ** Make the '-I file_spec' option required (without default <stdin>), *** reworked formats-detection and *** fix(?) clipboard reading (untested). ** Enhance cmd-line kv-pairs to accept data/paths as keys. *model: ** Schema-validator accepts and validates pandas(!). ** The /engine/fuel is given in lower(). ** Workaround discovered bug in modeljsonschema-validator exception: python-jsonschema/jsonschema#164 ** Add /params model and schema checks, /engine_points. ** Move model dump and validation-code into model-module. * ALL TESTS OK
1 parent a63fbd0 commit 6ba0e7a

File tree

6 files changed

+204
-82
lines changed

6 files changed

+204
-82
lines changed

fuefit/__init__.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,6 @@
3434
# Utilities
3535
#
3636

37-
def _json_default(o):
38-
if (isinstance(o, pd.DataFrame)):
39-
return json.loads(pd.DataFrame.to_json(o))
40-
else:
41-
return repr(o)
42-
43-
def json_dumps(obj):
44-
return json.dumps(obj, indent=2, default=_json_default)
45-
46-
4737
def str2bool(v):
4838
vv = v.lower()
4939
if (vv in ("yes", "true", "on")):

fuefit/main.py

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@
2828
import re
2929
import sys, os
3030
from textwrap import dedent
31-
import traceback
3231

32+
import ast
3333
import jsonpointer as jsonp
3434
import jsonschema as jsons
3535
import pandas as pd
3636

37-
from . import (_version, DEBUG, model, json_dumps, str2bool) # @UnusedImport
38-
37+
from . import (_version, DEBUG, model, str2bool) # @UnusedImport
38+
from .model import (json_dumps)
39+
from .model import validate_model
3940

4041
logging.basicConfig(level=logging.DEBUG)
4142
log = logging.getLogger(__file__)
@@ -55,7 +56,7 @@ def main(argv=None):
5556
*= : float
5657
?= : boolean
5758
:= : parsed as json
58-
;= : parsed as python (with eval())
59+
@= : parsed as python (with eval())
5960
6061
EXAMPLES:
6162
---------
@@ -65,21 +66,21 @@ def main(argv=None):
6566
...
6667
6768
## Calculate and print fitted engine map's parameters
68-
# for a PETROL vehicle with the above engine-point's CSV-table:
69-
>> %(prog)s -m fuel=PETROL -I engine.csv
69+
# for a petrol vehicle with the above engine-point's CSV-table:
70+
>> %(prog)s -m fuel=petrol -I engine.csv
7071
7172
## Assume PME column contained normalized-Power in Watts,
7273
# instead of P in kW:
73-
>> %(prog)s -m fuel=PETROL -I engine.csv -irenames X X 'Pnorm (w)'
74+
>> %(prog)s -m fuel=petrol -I engine.csv -irenames X X 'Pnorm (w)'
7475
7576
## Read the same table above but without header-row and
76-
# store results into Excel file:
77-
>> %(prog)s -m fuel=PETROL -I engine.csv --icolumns CM PME PMF -I engine_map.xlsx
77+
# store results into Excel file, 1st sheet:
78+
>> %(prog)s -m fuel=petrol -I engine.csv --icolumns CM PME PMF -I engine_map.xlsx sheetname+=0
7879
7980
## Supply as inline-json more model-values required for columns [RPM, P, FC]
8081
# read from <stdin> as json 2D-array of values (no headers).
8182
# and store results in UTF-8 regardless of platform's default encoding:
82-
>> %(prog)s -m '/engine:={"fuel":"PETROL", "stroke":15, "capacity":1359}' \\
83+
>> %(prog)s -m '/engine:={"fuel":"petrol", "stroke":15, "capacity":1359}' \\
8384
-I - file_frmt=JSON orient=values -c RPM P FC \\
8485
-O engine_map.txt encoding=UTF-8
8586
@@ -93,11 +94,11 @@ def main(argv=None):
9394
and the 2nd having 2 columns with no headers at all and
9495
the 1st column being 'Pnorm', then it, then use the following command:
9596
96-
>> %(prog)s -O engine_map -m fuel=PETROL \\
97-
-I=engine_1.xlsx \\
97+
>> %(prog)s -O engine_map -m fuel=petrol \\
98+
-I=engine_1.xlsx sheetname+=0 \\
9899
-c X X N 'Fuel consumption' X \\
99100
-r X X RPM 'FC(g/s)' X \\
100-
-I=engine_2.csv \\
101+
-I=engine_2.csv header@=None \\
101102
-c Pnorm X
102103
"""
103104

@@ -119,11 +120,15 @@ def main(argv=None):
119120

120121
DEBUG = bool(opts.debug)
121122

122-
if (DEBUG):
123+
if (DEBUG or opts.verbose > 1):
123124
log.setLevel(logging.DEBUG)
124125
else:
125-
log.setLevel(logging.INFO)
126-
log.info("Args: argv\nOpts: %s", opts)
126+
if opts.verbose == 1:
127+
log.setLevel(logging.INFO)
128+
else:
129+
log.setLevel(logging.WARNING)
130+
131+
log.debug("Args: %s\nOpts: %s", argv, opts)
127132

128133
opts = validate_file_opts(opts)
129134

@@ -134,7 +139,7 @@ def main(argv=None):
134139
log.info("Output-files: %s", outfiles)
135140

136141
mdl = build_model(opts, infiles)
137-
log.info("Input Model: %s", json_dumps(mdl))
142+
log.info("Input Model: %s", json_dumps(mdl, 'to_string'))
138143
mdl = validate_model(mdl)
139144

140145
except (SystemExit) as ex:
@@ -148,10 +153,16 @@ def main(argv=None):
148153
parser.exit(3, "%s: %s\n%s for help use --help\n"%(program_name, ex, indent))
149154
except jsons.ValidationError as ex:
150155
if DEBUG:
151-
log.exception('Invalid input model!')
156+
log.error('Invalid input model!', exc_info=ex)
152157
indent = len(program_name) * " "
153158
parser.exit(4, "%s: Model validation failed due to: %s\n%s for help use --help\n"%(program_name, ex, indent))
154159

160+
except jsonp.JsonPointerException as ex:
161+
if DEBUG:
162+
log.exception('Invalid model operation!')
163+
indent = len(program_name) * " "
164+
parser.exit(4, "%s: Model operation failed due to: %s\n%s for help use --help\n"%(program_name, ex, indent))
165+
155166

156167

157168
## The value of file_frmt=VALUE to decide which pandas.read_XXX() method to use.
@@ -161,7 +172,6 @@ def main(argv=None):
161172
('TXT', pd.read_csv),
162173
('XLS', pd.read_excel),
163174
('JSON', pd.read_json),
164-
('CLIPBOARD', pd.read_clipboard),
165175
])
166176
_known_file_exts = {
167177
'XLSX':'XLS'
@@ -193,11 +203,11 @@ def get_file_format_from_extension(fname):
193203
'*': float,
194204
'?': str2bool,
195205
':': json.loads,
196-
';': eval
206+
'@': ast.literal_eval ## best-effort security: http://stackoverflow.com/questions/3513292/python-make-eval-safe
197207
}
198208

199209

200-
_key_value_regex = re.compile(r'^\s*([A-Za-z]\w*)\s*([+*?:;]?)=\s*(.+?)\s*$')
210+
_key_value_regex = re.compile(r'^\s*([/_A-Za-z][\w/\.]*)\s*([+*?:@]?)=\s*(.+?)\s*$')
201211
def parse_key_value_pair(arg):
202212
"""Argument-type for syntax like: KEY [+*?:]= VALUE."""
203213

@@ -226,8 +236,6 @@ def parse_column_specifier(arg):
226236
raise argparse.ArgumentTypeError("Not a COLUMN_SPEC syntax: %s"%arg)
227237

228238

229-
FileSpec = collections.namedtuple('FileSpec', ('fname', 'file', 'frmt', 'path', 'append', 'kws'))
230-
231239
def validate_file_opts(opts):
232240
## Check number of input-files <--> related-opts
233241
#
@@ -249,7 +257,10 @@ def validate_file_opts(opts):
249257
return opts
250258

251259

260+
FileSpec = collections.namedtuple('FileSpec', ('fname', 'file', 'frmt', 'path', 'append', 'kws', 'read_method'))
261+
252262
def parse_many_file_args(many_file_args, filetype):
263+
253264
def parse_file_args(fname, *kv_args):
254265
frmt = _default_pandas_format
255266
dest = _default_df_dest
@@ -263,17 +274,25 @@ def parse_file_args(fname, *kv_args):
263274
if (frmt not in _pandas_formats):
264275
raise argparse.ArgumentTypeError("Unsupported pandas file_frmt: %s\n Set 'file_frmt=XXX' to one of %s" % (frmt, list(_pandas_formats.keys())[1:]))
265276

266-
if (frmt == 'CLIPBOARD'):
277+
if (fname == '+'):
267278
file = None
268279
fname = '<CLIPBOARD>'
280+
frmt = 'TABLE'
281+
method = pd.read_clipboard
269282
else:
270283
if (frmt == _default_pandas_format):
271284
if ('-' == fname):
272-
raise argparse.ArgumentTypeError("With <stdio> a concrete file_frmt is required! \n Set 'file_frmt=XXX' to one of %s" % (list(_pandas_formats.keys())[1:]))
285+
raise argparse.ArgumentTypeError("With <stdio> and <clipboard> a concrete file_frmt is required! \n Set 'file_frmt=XXX' to one of %s" % (list(_pandas_formats.keys())[1:]))
273286
frmt = get_file_format_from_extension(fname)
274287
if (not frmt):
275288
raise argparse.ArgumentTypeError("File(%s) has unknown extension, file_frmt is required! \n Set 'file_frmt=XXX' to one of %s" % (fname, list(_pandas_formats.keys())[1:]))
276-
file = argparse.FileType(filetype)(fname)
289+
290+
method = _pandas_formats[frmt]
291+
292+
if (method == pd.read_excel):
293+
file = fname
294+
else:
295+
file = argparse.FileType(filetype)(fname)
277296

278297
try:
279298
dest = pandas_kws.pop('model_path')
@@ -288,14 +307,15 @@ def parse_file_args(fname, *kv_args):
288307
except KeyError:
289308
pass
290309

291-
return FileSpec(fname, file, frmt, dest, append, pandas_kws)
310+
return FileSpec(fname, file, frmt, dest, append, pandas_kws, method)
292311

293312
return [parse_file_args(*file_args) for file_args in many_file_args]
294313

295314

296315
def load_file_as_df(filespec):
297316
# FileSpec(fname, file, frmt, path, append, kws)
298-
method = _pandas_formats[filespec.frmt]
317+
method = filespec.read_method
318+
log.debug('Reading file with: pandas.%s(%s, %s)', method.__name__, filespec.file, filespec.kws)
299319
dfin = method(filespec.file, **filespec.kws)
300320

301321
return dfin
@@ -306,7 +326,7 @@ def build_model(opts, infiles):
306326

307327
for filespec in infiles:
308328
dfin = load_file_as_df(filespec)
309-
log.info("+-input-file(%s):\n%s", filespec.fname, dfin)
329+
log.debug(" +-input-file(%s):\n%s", filespec.fname, dfin)
310330
jsonp.set_pointer(mdl, filespec.path, dfin)
311331

312332
model_overrides = opts.m
@@ -320,12 +340,6 @@ def build_model(opts, infiles):
320340
return mdl
321341

322342

323-
def validate_model(mdl):
324-
validator = model.model_validator()
325-
validator.validate(mdl)
326-
327-
return mdl
328-
329343
def build_args_parser(program_name, version, desc, epilog):
330344
version_string = '%%prog %s' % (version)
331345

@@ -360,7 +374,7 @@ def build_args_parser(program_name, version, desc, epilog):
360374
must either match them, be 1 (meaning use them for all files), or be totally absent
361375
(meaning use defaults for all files). """),
362376
action='append', nargs='+', required=True,
363-
# default=[('- file_frmt=%s model_path=%s'%('CSV', _default_df_dest)).split()],
377+
#default=[('- file_frmt=%s model_path=%s'%('CSV', _default_df_dest)).split()],
364378
metavar='ARG')
365379
grp_io.add_argument('-c', '--icolumns', help=dedent("""\
366380
describes the contents and the units of input file(s) (see --I).
@@ -443,8 +457,8 @@ def build_args_parser(program_name, version, desc, epilog):
443457

444458
grp_various = parser.add_argument_group('Various', 'Options controlling various other aspects.')
445459
#parser.add_argument('--gui', help='start in GUI mode', action='store_true')
446-
grp_various.add_argument("--debug", action="store_true", help="set debug level [default: %(default)s]", default=False)
447-
grp_various.add_argument("--verbose", action="count", default=0, help="set verbosity level [default: %(default)s]")
460+
grp_various.add_argument('-d', "--debug", action="store_true", help="set debug level [default: %(default)s]", default=False)
461+
grp_various.add_argument('-v', "--verbose", action="count", default=0, help="set verbosity level [default: %(default)s]")
448462
grp_various.add_argument("--version", action="version", version=version_string, help="prints version identifier of the program")
449463
grp_various.add_argument("--help", action="help", help='show this help message and exit')
450464

0 commit comments

Comments
 (0)