csvpath 0.0.504__tar.gz → 0.0.506__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {csvpath-0.0.504 → csvpath-0.0.506}/PKG-INFO +3 -2
- {csvpath-0.0.504 → csvpath-0.0.506}/config/config.ini +14 -2
- csvpath-0.0.506/csvpath/managers/integrations/sftp/sftp_sender.py +124 -0
- csvpath-0.0.506/csvpath/managers/integrations/sftpplus/arrival_handler.py +74 -0
- csvpath-0.0.506/csvpath/managers/integrations/sftpplus/sftpplus_listener.py +185 -0
- csvpath-0.0.506/csvpath/managers/integrations/sftpplus/transfer_creator.py +212 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/paths/paths_manager.py +3 -2
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result_serializer.py +2 -15
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/results_manager.py +12 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/results_registrar.py +3 -1
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/config.py +24 -8
- csvpath-0.0.506/csvpath/util/var_utility.py +117 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/pyproject.toml +3 -2
- {csvpath-0.0.504 → csvpath-0.0.506}/LICENSE +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/README.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/__init__.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/cli/__init__.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/cli/cli.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/cli/drill_down.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/csvpath.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/csvpaths.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/__init__.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/files/file_cacher.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/files/file_manager.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/files/file_metadata.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/files/file_registrar.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ckan/ckan.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ckan/ckan_listener.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ckan/datafile.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ckan/dataset.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/event.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/event_result.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/file_listener_ol.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/job.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/ol_listener.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/paths_listener_ol.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/result_listener_ol.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/results_listener_ol.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/run.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/run_listener_ol.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/run_state.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/sender.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/slack/event.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/slack/sender.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/listener.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/metadata.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/paths/paths_metadata.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/paths/paths_registrar.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/registrar.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/file_errors_reader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/file_lines_reader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/file_printouts_reader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/file_unmatched_reader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/readers.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result_file_reader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result_metadata.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result_registrar.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/results_metadata.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/run/run_listener_stdout.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/run/run_metadata.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/run/run_registrar.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/__init__.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/__init__.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/args.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/all.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/andf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/any.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/between.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/empty.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/exists.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/inf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/no.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/notf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/orf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/yes.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count_bytes.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count_headers.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count_lines.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count_scans.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/counter.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/every.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/has_matches.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/increment.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/tally.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/total_lines.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/dates/now.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/function.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/function_factory.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/function_finder.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/function_focus.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/append.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/collect.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/empty_stack.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/end.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/header_name.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/header_names_mismatch.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/headers.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/mismatch.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/replace.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/reset_headers.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/advance.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/after_blank.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/dups.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/first.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/first_line.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/last.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/stop.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/above.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/add.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/divide.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/equals.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/intf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/mod.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/multiply.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/round.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/subtotal.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/subtract.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/sum.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/misc/fingerprint.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/misc/importf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/misc/random.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/jinjaf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/print_line.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/print_queue.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/printf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/table.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/stats/minf.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/stats/percent.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/stats/percent_unique.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/stats/stdev.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/concat.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/length.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/lower.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/metaphone.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/regex.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/starts_with.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/strip.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/substring.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/upper.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/testing/debug.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/__init__.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/boolean.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/datef.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/decimal.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/nonef.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/string.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/type.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/validity/fail.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/validity/failed.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/validity/line.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/get.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/pushpop.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/put.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/track.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/variables.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/lark_parser.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/lark_transformer.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/matcher.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/__init__.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/equality.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/expression.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/header.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/matchable.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/qualified.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/reference.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/term.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/variable.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/exceptions.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/expression_encoder.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/expression_utility.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/lark_print_parser.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/print_parser.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/runtime_data_collector.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/explain_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/files_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/logic_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/mode_controller.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/print_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/return_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/run_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/source_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/transfer_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/unmatched_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/validation_mode.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/__init__.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/exceptions.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/parser.out +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/parsetab.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/scanner.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/scanning_lexer.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/cache.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/class_loader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/config_exception.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/error.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/exceptions.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/file_info.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/file_readers.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/file_writers.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/last_line_stats.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/line_counter.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/line_monitor.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/line_spooler.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/log_utility.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/metadata_parser.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/nos.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/pandas_data_reader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/printer.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/reference_parser.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_data_reader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_data_writer.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_fingerprinter.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_utils.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_xlsx_data_reader.py +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/asbool.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/assignment.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/comments.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/config.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/examples.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/files.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/above.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/advance.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/after_blank.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/all.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/andor.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/any.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/average.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/between.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/collect.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/correlate.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/count.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/count_bytes.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/count_headers.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/counter.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/date.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/empty.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/empty_stack.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/end.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/every.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/fail.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/fingerprint.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/first.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/get.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/has_dups.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/has_matches.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/header.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/header_name.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/header_names_mismatch.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/implementing_functions.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/import.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/in.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/increment.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/intf.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/jinja.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/last.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/line.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/line_number.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/max.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/metaphone.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/mismatch.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/no.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/not.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/now.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/percent_unique.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/pop.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/print.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/print_line.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/print_queue.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/random.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/regex.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/replace.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/reset_headers.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/stdev.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/stop.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/string_functions.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/subtotal.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/subtract.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/sum.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/tally.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/total_lines.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/track.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/types.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/variables.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/variables_and_headers.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/grammar.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/headers.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/ckan-logo-sm.png +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/csvpath-icon-sm.png +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/csvpath-logo-wordmark-tight-2.svg +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/logo-wordmark-3.svg +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/logo-wordmark-4.svg +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/logo-wordmark-white-on-black-trimmed-padded.png +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/logo-wordmark-white-trimmed.png +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/marquez-logo-sm.png +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/openlineage-logo-sm.png +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/paths.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/printing.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/qualifiers.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/references.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/terms.md +0 -0
- {csvpath-0.0.504 → csvpath-0.0.506}/docs/variables.md +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: csvpath
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.0.506
|
|
4
|
+
Summary: An edge governance framework for managing and validating CSV, Excel, and other tabular data files
|
|
5
5
|
Author: David Kershaw
|
|
6
6
|
Author-email: dk107dk@hotmail.com
|
|
7
7
|
Requires-Python: >=3.9,<4.0
|
|
@@ -33,6 +33,7 @@ Requires-Dist: lark (>=1.2.2,<2.0.0)
|
|
|
33
33
|
Requires-Dist: marquez-python (>=0.50.0,<0.51.0)
|
|
34
34
|
Requires-Dist: metaphone (>=0.6,<0.7)
|
|
35
35
|
Requires-Dist: openlineage-python (>=1.25.0,<2.0.0)
|
|
36
|
+
Requires-Dist: paramiko (>=3.5.0,<4.0.0)
|
|
36
37
|
Requires-Dist: ply (>=3.11,<4.0)
|
|
37
38
|
Requires-Dist: pylightxl (>=1.61,<2.0)
|
|
38
39
|
Requires-Dist: pytest (>=8.3.3,<9.0.0)
|
|
@@ -26,12 +26,18 @@ imports = config/functions.imports
|
|
|
26
26
|
|
|
27
27
|
[listeners]
|
|
28
28
|
groups =
|
|
29
|
-
#slack, marquez, ckan
|
|
29
|
+
#slack, marquez, ckan, sftp, sftpplus
|
|
30
|
+
|
|
31
|
+
# add sftpplus to the list of groups above to automate registration and named-paths group runs on file arrival at an SFTPPlus server
|
|
32
|
+
sftpplus.paths = from csvpath.managers.integrations.sftpplus.sftpplus_listener import SftpPlusListener
|
|
33
|
+
|
|
34
|
+
# add sftp to the list of groups above to push results to an sftp account
|
|
35
|
+
sftp.results = from csvpath.managers.integrations.sftp.sftp_listener import SftpListener
|
|
30
36
|
|
|
31
37
|
# add ckan to the list of groups above for alerts to slack webhooks
|
|
32
38
|
ckan.results = from csvpath.managers.integrations.ckan.ckan_listener import CkanListener
|
|
33
39
|
|
|
34
|
-
#add marquez to the list of groups above for OpenLineage events to a
|
|
40
|
+
#add marquez to the list of groups above for OpenLineage events to a Marquez server
|
|
35
41
|
marquez.file = from csvpath.managers.integrations.ol.file_listener_ol import OpenLineageFileListener
|
|
36
42
|
marquez.paths = from csvpath.managers.integrations.ol.paths_listener_ol import OpenLineagePathsListener
|
|
37
43
|
marquez.result = from csvpath.managers.integrations.ol.result_listener_ol import OpenLineageResultListener
|
|
@@ -43,6 +49,12 @@ slack.paths = from csvpath.managers.integrations.slack.sender import SlackSender
|
|
|
43
49
|
slack.result = from csvpath.managers.integrations.slack.sender import SlackSender
|
|
44
50
|
slack.results = from csvpath.managers.integrations.slack.sender import SlackSender
|
|
45
51
|
|
|
52
|
+
[sftpplus]
|
|
53
|
+
admin_user = SFTPPLUS_ADMIN_USERNAME
|
|
54
|
+
admin_password = SFTPPLUS_ADMIN_PASSWORD
|
|
55
|
+
server = SFTPPLUS_SERVER
|
|
56
|
+
port = SFTPPLUS_PORT
|
|
57
|
+
|
|
46
58
|
[ckan]
|
|
47
59
|
server = http://localhost:80
|
|
48
60
|
api_token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI3akJwc1ZuSkVrZm1aNnBtVTJfTW5CNlJXZ211YjdOOHVXZ1l1cUFDa0Q4IiwiaWF0IjoxNzM0NzE4NDQ3fQ.QXWXoJoSxVES4NwXYBteYUD7enX9D5T2htmETLGFzrs
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import paramiko
|
|
3
|
+
from csvpath.managers.metadata import Metadata
|
|
4
|
+
from csvpath.managers.results.results_metadata import ResultsMetadata
|
|
5
|
+
from csvpath.managers.results.results_registrar import ResultsRegistrar
|
|
6
|
+
from csvpath.managers.results.result import Result
|
|
7
|
+
from csvpath.managers.listener import Listener
|
|
8
|
+
from csvpath.util.nos import Nos
|
|
9
|
+
from csvpath.util.var_utility import VarUtility
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
#
|
|
13
|
+
# this class is for sending results to an SFTP location. it also run on
|
|
14
|
+
# result updates, but for now we'll just do results updates and loop over
|
|
15
|
+
# all the results.
|
|
16
|
+
#
|
|
17
|
+
# sftp metadata fields and values are like e.g.:
|
|
18
|
+
# sftp-server: localhost
|
|
19
|
+
# sftp-port: 22
|
|
20
|
+
# sftp-user: LOCAL_SFTP_USER
|
|
21
|
+
# sftp-password: LOCAL_SFTP_PASSWORD
|
|
22
|
+
# sftp-target-path: my_data/
|
|
23
|
+
# sftp-files: data.csv > data.csv, unmatched.csv > var|unmatched_filename, errors.json > errors.json
|
|
24
|
+
# sftp-original-data: send
|
|
25
|
+
#
|
|
26
|
+
class SftpSender(Listener, threading.Thread):
|
|
27
|
+
def __init__(self, *, config=None):
|
|
28
|
+
super().__init__(config)
|
|
29
|
+
self._server = None
|
|
30
|
+
self._port = None
|
|
31
|
+
self._user = None
|
|
32
|
+
self._password = None
|
|
33
|
+
self._target_path = None
|
|
34
|
+
self._files = []
|
|
35
|
+
self._send_original = False
|
|
36
|
+
self.csvpaths = None
|
|
37
|
+
self.result = None
|
|
38
|
+
self.metadata = None
|
|
39
|
+
self.results = None
|
|
40
|
+
|
|
41
|
+
def _collect_fields(self) -> None:
|
|
42
|
+
self._server = VarUtility.get_str(self.result, "sftp-server")
|
|
43
|
+
self._port = VarUtility.get_int(self.result, "sftp-port")
|
|
44
|
+
self._user = VarUtility.get_str(self.result, "sftp-user")
|
|
45
|
+
self._password = VarUtility.get_str(self.result, "sftp-password")
|
|
46
|
+
self._target_path = VarUtility.get_str(self.result, "sftp-target-path")
|
|
47
|
+
self._original = VarUtility.get_bool(self.result, "sftp-original-data")
|
|
48
|
+
self._files = VarUtility.get_value_pairs(self.result, "sftp-files")
|
|
49
|
+
|
|
50
|
+
def run(self):
|
|
51
|
+
self.csvpaths.logger.info("Checking for requests to send result files by SFTP")
|
|
52
|
+
self.results = self.csvpaths.results_manager.get_named_results(
|
|
53
|
+
self.metadata.named_results_name
|
|
54
|
+
)
|
|
55
|
+
for result in self.results:
|
|
56
|
+
self.result = result
|
|
57
|
+
self._collect_fields()
|
|
58
|
+
self._metadata_update()
|
|
59
|
+
|
|
60
|
+
def metadata_update(self, mdata: Metadata) -> None:
|
|
61
|
+
if mdata is None:
|
|
62
|
+
raise ValueError("Metadata cannot be None")
|
|
63
|
+
if not isinstance(mdata, ResultsMetadata):
|
|
64
|
+
if self.csvpaths:
|
|
65
|
+
self.csvpaths.logger.warning(
|
|
66
|
+
"SftpSender only listens for results events. Other event types are ignored."
|
|
67
|
+
)
|
|
68
|
+
return
|
|
69
|
+
if mdata.status == ResultsRegistrar.COMPLETE:
|
|
70
|
+
self.metadata = mdata
|
|
71
|
+
self.start()
|
|
72
|
+
|
|
73
|
+
def _metadata_update(self) -> None:
|
|
74
|
+
if (
|
|
75
|
+
self._files is None or len(self._files) == 0
|
|
76
|
+
) and self._send_original is not True:
|
|
77
|
+
# no files to send and not sending the original data means we're done
|
|
78
|
+
return
|
|
79
|
+
files = [
|
|
80
|
+
"data.csv",
|
|
81
|
+
"unmatched.csv",
|
|
82
|
+
"errors.json",
|
|
83
|
+
"vars.json",
|
|
84
|
+
"meta.json",
|
|
85
|
+
"printouts.txt",
|
|
86
|
+
"manifest.json",
|
|
87
|
+
]
|
|
88
|
+
sep = Nos(self.metadata.run_home).sep
|
|
89
|
+
client = paramiko.SSHClient()
|
|
90
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
91
|
+
try:
|
|
92
|
+
client.connect(self._server, self._port, self._user, self._password)
|
|
93
|
+
sftp = client.open_sftp()
|
|
94
|
+
self.csvpaths.logger.info("Preparing to send %s files", len(self._files))
|
|
95
|
+
try:
|
|
96
|
+
sftp.stat(self._target_path)
|
|
97
|
+
except FileNotFoundError:
|
|
98
|
+
sftp.mkdir(self._target_path)
|
|
99
|
+
for pair in self._files:
|
|
100
|
+
file = pair[0]
|
|
101
|
+
to = pair[1]
|
|
102
|
+
if file not in files:
|
|
103
|
+
raise ValueError("File name {file} is not in {files}")
|
|
104
|
+
if to is None:
|
|
105
|
+
raise ValueError("File name {file} has no destination")
|
|
106
|
+
path = f"/Users/davidkershaw/dev/csvpath/{self.metadata.run_home}{sep}{self.result.identity_or_index}{sep}{file}"
|
|
107
|
+
remote_path = f"{self._target_path}/{to}"
|
|
108
|
+
self.csvpaths.logger.info("Putting %s to %s", path, remote_path)
|
|
109
|
+
sftp.put(path, remote_path)
|
|
110
|
+
#
|
|
111
|
+
# send the original file if we need to. this will always be the normative
|
|
112
|
+
# original, w/o regard to source-mode or by_line chaining or to rewind.
|
|
113
|
+
#
|
|
114
|
+
if self._original is True:
|
|
115
|
+
path = self.results[0].csvpath.scanner.filename
|
|
116
|
+
if path is None:
|
|
117
|
+
raise ValueError("Filename of first result cannot be None")
|
|
118
|
+
remote_file = path[path.rfind(sep) + 1 :]
|
|
119
|
+
remote_path = f"{self._target_path}/{remote_file}"
|
|
120
|
+
sftp.put(path, remote_path)
|
|
121
|
+
|
|
122
|
+
sftp.close()
|
|
123
|
+
finally:
|
|
124
|
+
client.close()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
from csvpath import CsvPaths
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
#
|
|
7
|
+
# this class is executed when a file arrives on a transfer set up
|
|
8
|
+
# by TransferCreator to handle inbound named-files.
|
|
9
|
+
#
|
|
10
|
+
class SftpPlusArrivalHandler:
|
|
11
|
+
def __init__(self, path):
|
|
12
|
+
self._csvpaths = CsvPaths()
|
|
13
|
+
self._path = path
|
|
14
|
+
self._named_file_name = None
|
|
15
|
+
self._named_paths_name = None
|
|
16
|
+
self._run_method = None
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def path(self) -> str:
|
|
20
|
+
return self._path
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def run_method(self) -> str:
|
|
24
|
+
return self.run_method
|
|
25
|
+
|
|
26
|
+
@run_method.setter
|
|
27
|
+
def run_method(self, n: str) -> None:
|
|
28
|
+
self.run_method = n
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def named_file_name(self) -> str:
|
|
32
|
+
return self._named_file_name
|
|
33
|
+
|
|
34
|
+
@named_file_name.setter
|
|
35
|
+
def named_file_name(self, n: str) -> None:
|
|
36
|
+
self._named_file_name = n
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def named_paths_name(self) -> str:
|
|
40
|
+
return self._named_paths_name
|
|
41
|
+
|
|
42
|
+
@named_paths_name.setter
|
|
43
|
+
def named_paths_name(self, n: str) -> None:
|
|
44
|
+
self._named_paths_name = n
|
|
45
|
+
|
|
46
|
+
def process_arrival(self) -> None:
|
|
47
|
+
#
|
|
48
|
+
# register the file
|
|
49
|
+
#
|
|
50
|
+
self._csvpaths.file_manager.add_named_file(
|
|
51
|
+
name=self.named_file_name, path=self.path
|
|
52
|
+
)
|
|
53
|
+
#
|
|
54
|
+
# do the run
|
|
55
|
+
#
|
|
56
|
+
m = self.run_method
|
|
57
|
+
if m is None or self.run_method == "collect_paths":
|
|
58
|
+
self._csvpaths.collect_paths(
|
|
59
|
+
filename=self.named_file_name, pathsname=self.named_paths_name
|
|
60
|
+
)
|
|
61
|
+
elif m == "fast_forward_paths":
|
|
62
|
+
self._csvpaths.fast_forward_paths(
|
|
63
|
+
filename=self.named_file_name, pathsname=self.named_paths_name
|
|
64
|
+
)
|
|
65
|
+
elif m == "collect_by_line":
|
|
66
|
+
self._csvpaths.collect_by_line(
|
|
67
|
+
filename=self.named_file_name, pathsname=self.named_paths_name
|
|
68
|
+
)
|
|
69
|
+
elif m == "fast_forward_by_line":
|
|
70
|
+
self._csvpaths.fast_forward_by_line(
|
|
71
|
+
filename=self.named_file_name, pathsname=self.named_paths_name
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
self._csvpaths.config.error("Run method is incorrect: {m}")
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import threading
|
|
4
|
+
import paramiko
|
|
5
|
+
from tempfile import NamedTemporaryFile
|
|
6
|
+
from csvpath.managers.metadata import Metadata
|
|
7
|
+
from csvpath.managers.results.results_metadata import PathsMetadata
|
|
8
|
+
from csvpath.managers.listener import Listener
|
|
9
|
+
from csvpath.util.var_utility import VarUtility
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
#
|
|
13
|
+
# this class listens for paths events. when it gets one it generates
|
|
14
|
+
# a file of instructions and sends them to an SFTPPlus mailbox account.
|
|
15
|
+
# a transfer on the landing dir moves the instructions to a holding
|
|
16
|
+
# location for future reference: `user`/csvpath_messages/handled
|
|
17
|
+
#
|
|
18
|
+
# before the move happens a script runs to process the instructions.
|
|
19
|
+
# the instructions set up a transfer for the named-paths group's
|
|
20
|
+
# expected file arrivals.
|
|
21
|
+
#
|
|
22
|
+
# that transfer executes a script that loads the files as named-files and
|
|
23
|
+
# executes a run of the named-paths on the new named-file. it then moves
|
|
24
|
+
# the arrived file to a holding location for process debugging reference.
|
|
25
|
+
# the single-source authorative file is at this point in the named-files
|
|
26
|
+
# inputs directory, whereever that is configured.
|
|
27
|
+
#
|
|
28
|
+
class SftpPlusListener(Listener, threading.Thread):
|
|
29
|
+
def __init__(self, *, config=None):
|
|
30
|
+
super().__init__(config)
|
|
31
|
+
self._server = None
|
|
32
|
+
self._port = None
|
|
33
|
+
self._user = None
|
|
34
|
+
self._password = None
|
|
35
|
+
self._target_path = "csvpath_messages"
|
|
36
|
+
self._delete_on_success = True
|
|
37
|
+
self._publish = False
|
|
38
|
+
self._expected_file_name = None
|
|
39
|
+
self._execute_before_script_name = None
|
|
40
|
+
self.csvpaths = None
|
|
41
|
+
self.result = None
|
|
42
|
+
self.metadata = None
|
|
43
|
+
self.results = None
|
|
44
|
+
|
|
45
|
+
def _collect_fields(self) -> None:
|
|
46
|
+
# directives stuff:
|
|
47
|
+
self._publish = VarUtility.get_bool(self.result, "sftpplus-publish")
|
|
48
|
+
self._expected_file_name = VarUtility.get_str(
|
|
49
|
+
self.result, "sftpplus-named-file-name"
|
|
50
|
+
)
|
|
51
|
+
self._method = VarUtility.get_str(self.result, "sftpplus-run-method")
|
|
52
|
+
# config.ini stuff:
|
|
53
|
+
self._user = self.csvpath.config.get(section="sftpplus", name="admin_user")
|
|
54
|
+
if self._user is None:
|
|
55
|
+
raise ValueError("SFTPPlus Admin username cannot be None")
|
|
56
|
+
if self._user.isupper():
|
|
57
|
+
self._user = os.getenv(self._user)
|
|
58
|
+
self._password = self.csvpath.config.get(
|
|
59
|
+
section="sftpplus", name="admin_password"
|
|
60
|
+
)
|
|
61
|
+
if self._password is None:
|
|
62
|
+
raise ValueError("SFTPPlus Admin password cannot be None")
|
|
63
|
+
if self._password.isupper():
|
|
64
|
+
self._password = os.getenv(self._password)
|
|
65
|
+
self._server = self.csvpath.config.get(section="sftpplus", name="server")
|
|
66
|
+
if self._server is None:
|
|
67
|
+
raise ValueError("SFTPPlus server cannot be None")
|
|
68
|
+
if self._server.isupper():
|
|
69
|
+
self._server = os.getenv(self._server)
|
|
70
|
+
self._port = self.csvpath.config.get(section="sftpplus", name="port")
|
|
71
|
+
if self._port is None:
|
|
72
|
+
raise ValueError("SFTPPlus port cannot be None")
|
|
73
|
+
if self._port.isupper():
|
|
74
|
+
self._port = os.getenv(self._port)
|
|
75
|
+
self._delete_on_success = self.csvpath.config.get(
|
|
76
|
+
section="sftpplus",
|
|
77
|
+
name="sftpplus-delete-on-success",
|
|
78
|
+
default=self._delete_on_success,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def run_method(self) -> str:
|
|
83
|
+
if self._method is None or self._method not in [
|
|
84
|
+
"collect_paths",
|
|
85
|
+
"fast_forward_paths",
|
|
86
|
+
"collect_by_line",
|
|
87
|
+
"fast_forward_by_line",
|
|
88
|
+
]:
|
|
89
|
+
self.csvpaths.logger.warning(
|
|
90
|
+
"No acceptable sftpplus-run-method found by SftpSender for {self.metadata.named_paths_name}: {self._method}. Defaulting to collect_paths."
|
|
91
|
+
)
|
|
92
|
+
self._method = "collect_paths"
|
|
93
|
+
return self._method
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def scripts_base(self) -> str:
|
|
97
|
+
return self.csvpaths.config.get(section="sftppath", name="scripts_base")
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def script_dir(self) -> str:
|
|
101
|
+
return self.csvpaths.config.get(section="sftppath", name="scripts_dir")
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def execute_before_script_name(self) -> str:
|
|
105
|
+
return self.csvpaths.config.get(
|
|
106
|
+
section="sftppath", name="execute_before_script_name"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def run(self):
|
|
110
|
+
self.csvpaths.logger.info("Checking for requests to send result files by SFTP")
|
|
111
|
+
self._metadata_update()
|
|
112
|
+
|
|
113
|
+
def metadata_update(self, mdata: Metadata) -> None:
|
|
114
|
+
if mdata is None:
|
|
115
|
+
raise ValueError("Metadata cannot be None")
|
|
116
|
+
if not isinstance(mdata, PathsMetadata):
|
|
117
|
+
if self.csvpaths:
|
|
118
|
+
self.csvpaths.logger.warning(
|
|
119
|
+
"SftpplusListener only listens for paths events. Other event types are ignored."
|
|
120
|
+
)
|
|
121
|
+
self.metadata = mdata
|
|
122
|
+
self.start()
|
|
123
|
+
|
|
124
|
+
def _metadata_update(self) -> None:
|
|
125
|
+
self._collect_fields()
|
|
126
|
+
msg = self._create_instructions()
|
|
127
|
+
self._send_message(msg)
|
|
128
|
+
|
|
129
|
+
def _send_message(self, msg: dict) -> None:
|
|
130
|
+
#
|
|
131
|
+
# write instructions message into a temp file
|
|
132
|
+
#
|
|
133
|
+
with NamedTemporaryFile(mode="w+t", delete_on_close=False) as file:
|
|
134
|
+
json.dump(msg, file, indent=2)
|
|
135
|
+
file.close()
|
|
136
|
+
file.seek(0)
|
|
137
|
+
|
|
138
|
+
client = paramiko.SSHClient()
|
|
139
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
140
|
+
try:
|
|
141
|
+
client.connect(self._server, self._port, self._user, self._password)
|
|
142
|
+
sftp = client.open_sftp()
|
|
143
|
+
self.csvpaths.logger.info(
|
|
144
|
+
"SFTPPlus listener prepping instruction to %s",
|
|
145
|
+
f"{self._user}/{self._target_path}",
|
|
146
|
+
)
|
|
147
|
+
#
|
|
148
|
+
# create the remote dir, in the messages account, if needed.
|
|
149
|
+
#
|
|
150
|
+
try:
|
|
151
|
+
sftp.stat(self._target_path)
|
|
152
|
+
except FileNotFoundError:
|
|
153
|
+
sftp.mkdir(self._target_path)
|
|
154
|
+
#
|
|
155
|
+
# land the file at the UUID so that if anything weird we'll only ever
|
|
156
|
+
# interfere with ourselves.
|
|
157
|
+
#
|
|
158
|
+
remote_path = f"{self._target_path}/{self.metadata.uuid_string}.txt"
|
|
159
|
+
self.csvpaths.logger.info("Putting %s to %s", file, remote_path)
|
|
160
|
+
sftp.putfo(file, remote_path)
|
|
161
|
+
sftp.close()
|
|
162
|
+
finally:
|
|
163
|
+
client.close()
|
|
164
|
+
|
|
165
|
+
def _create_instructions(self) -> dict:
|
|
166
|
+
#
|
|
167
|
+
# SFTPPLUS TRANSFER SETUP STUFF
|
|
168
|
+
# we are making the file-receiving transfer, not the message-receiving
|
|
169
|
+
# transfer. this will be used by the message-receiving transfer to prep
|
|
170
|
+
# the landing site for new files to be run against this named-paths.
|
|
171
|
+
#
|
|
172
|
+
msg = {}
|
|
173
|
+
msg["name"] = self.metadata.named_paths_name
|
|
174
|
+
msg["method"] = self.metadata.named_paths_name
|
|
175
|
+
msg[
|
|
176
|
+
"execute_before"
|
|
177
|
+
] = f"{self.scripts_base}/{self.scripts_dir}/{self.execute_before_script_name}"
|
|
178
|
+
msg["delete_source_on_success"] = f"{self._delete_on_success}"
|
|
179
|
+
msg["source_uuid"] = "DEFAULT-LOCAL-FILESYSTEM"
|
|
180
|
+
msg["source_path"] = f"{self._expected_file_name}"
|
|
181
|
+
msg["destination_uuid"] = "DEFAULT-LOCAL-FILESYSTEM"
|
|
182
|
+
msg["destination_path"] = f"{self._expected_file_name}/handled"
|
|
183
|
+
msg["description"] = f"{self.metadata.uuid_string}"
|
|
184
|
+
msg["named_file_name"] = f"{self._expected_file_name}"
|
|
185
|
+
msg["publish"] = f"{self._publish}"
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
from csvpath import CsvPaths
|
|
5
|
+
from csvpath.util.config import Config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# this class listens for messages. when it gets one it generates
|
|
10
|
+
# instructions for admin-shell.
|
|
11
|
+
#
|
|
12
|
+
# we also generate another script for the new transfer. that script
|
|
13
|
+
# loads the files as named-files and executes a run of the named-paths
|
|
14
|
+
# on the new named-file.
|
|
15
|
+
#
|
|
16
|
+
# it then moves the arrived file to a holding location for process
|
|
17
|
+
# debugging reference. the single-source authorative file is at this
|
|
18
|
+
# point in the named-files inputs directory, whereever that is
|
|
19
|
+
# configured.
|
|
20
|
+
#
|
|
21
|
+
class SftpPlusTransferCreator:
|
|
22
|
+
CSVPATH_ADMIN_PASSWORD = "CSVPATH_ADMIN_PASSWORD"
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._csvpaths = CsvPaths()
|
|
26
|
+
self._path = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def message_path(self) -> str:
|
|
30
|
+
return self._path
|
|
31
|
+
|
|
32
|
+
@message_path.setter
|
|
33
|
+
def message_path(self, p: str) -> None:
|
|
34
|
+
self._path = p
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def admin_username(self) -> str:
|
|
38
|
+
n = os.getenv(SftpPlusTransferCreator.CSVPATH_ADMIN_PASSWORD)
|
|
39
|
+
if n is not None:
|
|
40
|
+
return n
|
|
41
|
+
return self.config["sftpplus"]["admin_username"]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def admin_password(self) -> str:
|
|
45
|
+
pw = os.getenv(SftpPlusTransferCreator.CSVPATH_ADMIN_PASSWORD)
|
|
46
|
+
if pw is not None:
|
|
47
|
+
return pw
|
|
48
|
+
return self.config["sftpplus"]["admin_password"]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def config(self) -> Config:
|
|
52
|
+
return self._csvpaths.config
|
|
53
|
+
|
|
54
|
+
def process_message(self, msg_path) -> None:
|
|
55
|
+
#
|
|
56
|
+
# loads method as a single string
|
|
57
|
+
#
|
|
58
|
+
msg = self._get_message()
|
|
59
|
+
#
|
|
60
|
+
# the named-path uuid is in the message's (and transfer's) description field
|
|
61
|
+
# iterate the existing transfers looking for a description matching the named-paths
|
|
62
|
+
# group's uuid
|
|
63
|
+
#
|
|
64
|
+
tuuid = self._find_existing_transfer(msg)
|
|
65
|
+
#
|
|
66
|
+
# if tuuid exists we update the existing transfer
|
|
67
|
+
# otherwise we create a new transfer.
|
|
68
|
+
#
|
|
69
|
+
if tuuid is None:
|
|
70
|
+
tuuid = self._create_new_transfer(msg=msg)
|
|
71
|
+
else:
|
|
72
|
+
self._update_existing_transfer(tuuid=tuuid, msg=msg)
|
|
73
|
+
#
|
|
74
|
+
# generate the script that will load the named-file and run the named-paths when
|
|
75
|
+
# a new file arrives at the transfer.
|
|
76
|
+
#
|
|
77
|
+
self._generate_and_place_scripts(msg)
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# ===================
|
|
81
|
+
#
|
|
82
|
+
def _get_message(self) -> dict:
|
|
83
|
+
msg = None
|
|
84
|
+
with open(self.message_path, "r", encoding="utf-8") as file:
|
|
85
|
+
msg = json.load(file)
|
|
86
|
+
uuid = msg.get("uuid")
|
|
87
|
+
if uuid is None:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"uuid of named-paths group must be present in transfer setup message: {msg}"
|
|
90
|
+
)
|
|
91
|
+
#
|
|
92
|
+
# any other validations here
|
|
93
|
+
#
|
|
94
|
+
return msg
|
|
95
|
+
|
|
96
|
+
def _cmd(self, cmd: str) -> str:
|
|
97
|
+
c = """echo {self.admin_password} | ./bin/admin-shell.sh -k -u {self.admin_username} -p - {cmd} """
|
|
98
|
+
return c
|
|
99
|
+
|
|
100
|
+
def _find_existing_transfer(self, msg: dict) -> str:
|
|
101
|
+
#
|
|
102
|
+
# we use admin-shell's show transfer command to find our uuid match in
|
|
103
|
+
# the description field. if we find that we return the transfer's uuid.
|
|
104
|
+
# if the transfer exists we want to update it.
|
|
105
|
+
#
|
|
106
|
+
# create the command:
|
|
107
|
+
cmd = self._cmd("show transfer")
|
|
108
|
+
# run the command
|
|
109
|
+
out = self._run_cmd(cmd)
|
|
110
|
+
# parse the list
|
|
111
|
+
tuuid = None
|
|
112
|
+
ts = out.split("--------------------------------------------------")
|
|
113
|
+
for t in ts:
|
|
114
|
+
if t.find(msg["uuid"]) > -1:
|
|
115
|
+
i = t.find("uuid = ")
|
|
116
|
+
tuuid = t[i + 8 : t.find('"', start=i + 9)]
|
|
117
|
+
return tuuid
|
|
118
|
+
|
|
119
|
+
def _run_cmd(self, cmd: str) -> str:
|
|
120
|
+
parts = cmd.split(" ")
|
|
121
|
+
result = subprocess.run(parts, capture_output=True, text=True)
|
|
122
|
+
code = result.returncode
|
|
123
|
+
output = result.stdout
|
|
124
|
+
error = result.stderr
|
|
125
|
+
print(f"_run_command: code: {code}, error: {error}")
|
|
126
|
+
print(f"_run_command: output: {output}")
|
|
127
|
+
return output
|
|
128
|
+
|
|
129
|
+
def _create_transfer(self, name: str) -> str:
|
|
130
|
+
c = self._cmd(f"add transfer {name}")
|
|
131
|
+
o = self._run_cmd(c)
|
|
132
|
+
#
|
|
133
|
+
# output is like:
|
|
134
|
+
# New transfers created with UUID: f6ec10a0-baff-449d-9ba2-f89748b10dd4
|
|
135
|
+
#
|
|
136
|
+
i = o.find("UUID: ")
|
|
137
|
+
tuuid = o[i + 1 :]
|
|
138
|
+
print(f"_create_transfer: output: {o}")
|
|
139
|
+
print(f"_create_transfer: tuuid: {tuuid}")
|
|
140
|
+
return tuuid
|
|
141
|
+
|
|
142
|
+
def _create_new_transfer(self, *, msg: dict) -> str:
|
|
143
|
+
# create the commands
|
|
144
|
+
tuuid = self._create_transfer(msg["named_file_name"])
|
|
145
|
+
cmds = [
|
|
146
|
+
self._cmd(
|
|
147
|
+
f"configure transfer {tuuid} execute_before = {msg['execute_before']}"
|
|
148
|
+
),
|
|
149
|
+
self._cmd(
|
|
150
|
+
f"configure transfer {tuuid} delete_source_on_success = {msg['delete_source_on_success']}"
|
|
151
|
+
),
|
|
152
|
+
self._cmd(f"configure transfer {tuuid} source_uuid = {msg['source_uuid']}"),
|
|
153
|
+
self._cmd(f"configure transfer {tuuid} source_path = {msg['source_path']}"),
|
|
154
|
+
self._cmd(
|
|
155
|
+
f"configure transfer {tuuid} destination_uuid = {msg['destination_uuid']}"
|
|
156
|
+
),
|
|
157
|
+
self._cmd(
|
|
158
|
+
f"configure transfer {tuuid} destination_path = {msg['destination_path']}"
|
|
159
|
+
),
|
|
160
|
+
self._cmd(f"configure transfer {tuuid} enabled = {msg['publish']}"),
|
|
161
|
+
]
|
|
162
|
+
for cmd in cmds:
|
|
163
|
+
self._run_cmd(cmd)
|
|
164
|
+
|
|
165
|
+
def _update_existing_transfer(self, *, tuuid: str, msg: dict) -> None:
|
|
166
|
+
cmds = [
|
|
167
|
+
#
|
|
168
|
+
# we'll take execute_before to give us a relatively easy way to allow for
|
|
169
|
+
# the script changing.
|
|
170
|
+
#
|
|
171
|
+
self._cmd(
|
|
172
|
+
f"configure transfer {tuuid} execute_before = {msg['execute_before']}"
|
|
173
|
+
),
|
|
174
|
+
self._cmd(
|
|
175
|
+
f"configure transfer {tuuid} delete_source_on_success = {msg['delete_source_on_success']}"
|
|
176
|
+
),
|
|
177
|
+
self._cmd(f"configure transfer {tuuid} enabled = {msg['publish']}"),
|
|
178
|
+
]
|
|
179
|
+
for cmd in cmds:
|
|
180
|
+
self._run_cmd(cmd)
|
|
181
|
+
|
|
182
|
+
def _generate_and_place_scripts(self, msg: dict) -> str:
|
|
183
|
+
path = msg["execute_before"]
|
|
184
|
+
#
|
|
185
|
+
# we may need a setting for using poetry vs. pip, etc.
|
|
186
|
+
#
|
|
187
|
+
s = """
|
|
188
|
+
poetry run python transfer_creator_main.py "$1"
|
|
189
|
+
"""
|
|
190
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
191
|
+
file.write(s)
|
|
192
|
+
#
|
|
193
|
+
# do we need to +x the script?
|
|
194
|
+
#
|
|
195
|
+
#
|
|
196
|
+
# create the main.py that uses the handler to add the new named-file
|
|
197
|
+
# and run the named-paths group
|
|
198
|
+
#
|
|
199
|
+
s = """
|
|
200
|
+
import sys
|
|
201
|
+
from csvpath.managers.integrations.sftpplus.arrival_handler import SftpPlusArrivalHandler
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
path = sys.argv[1]
|
|
205
|
+
h = SftpPlusArrivalHandler(path)
|
|
206
|
+
h.named_file_name = "{msg['named_file_name']}"
|
|
207
|
+
h.run_method = "{msg['method']}"
|
|
208
|
+
h.named_paths_name = "{msg['name']}"
|
|
209
|
+
h.process_arrival()
|
|
210
|
+
"""
|
|
211
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
212
|
+
file.write(s)
|
|
@@ -39,7 +39,7 @@ class PathsManager:
|
|
|
39
39
|
|
|
40
40
|
def named_paths_home(self, name: NamedPathsName) -> str:
|
|
41
41
|
home = os.path.join(self.named_paths_dir, name)
|
|
42
|
-
if not Nos(home).
|
|
42
|
+
if not Nos(home).dir_exists():
|
|
43
43
|
Nos(home).makedirs()
|
|
44
44
|
return home
|
|
45
45
|
|
|
@@ -198,7 +198,8 @@ class PathsManager:
|
|
|
198
198
|
j = ""
|
|
199
199
|
with DataFileReader(jsonpath) as file:
|
|
200
200
|
j = file.read()
|
|
201
|
-
|
|
201
|
+
p = os.path.join(home, "definition.json")
|
|
202
|
+
with DataFileWriter(path=p) as writer:
|
|
202
203
|
writer.write(j)
|
|
203
204
|
|
|
204
205
|
@property
|