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.
Files changed (303) hide show
  1. {csvpath-0.0.504 → csvpath-0.0.506}/PKG-INFO +3 -2
  2. {csvpath-0.0.504 → csvpath-0.0.506}/config/config.ini +14 -2
  3. csvpath-0.0.506/csvpath/managers/integrations/sftp/sftp_sender.py +124 -0
  4. csvpath-0.0.506/csvpath/managers/integrations/sftpplus/arrival_handler.py +74 -0
  5. csvpath-0.0.506/csvpath/managers/integrations/sftpplus/sftpplus_listener.py +185 -0
  6. csvpath-0.0.506/csvpath/managers/integrations/sftpplus/transfer_creator.py +212 -0
  7. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/paths/paths_manager.py +3 -2
  8. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result_serializer.py +2 -15
  9. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/results_manager.py +12 -0
  10. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/results_registrar.py +3 -1
  11. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/config.py +24 -8
  12. csvpath-0.0.506/csvpath/util/var_utility.py +117 -0
  13. {csvpath-0.0.504 → csvpath-0.0.506}/pyproject.toml +3 -2
  14. {csvpath-0.0.504 → csvpath-0.0.506}/LICENSE +0 -0
  15. {csvpath-0.0.504 → csvpath-0.0.506}/README.md +0 -0
  16. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/__init__.py +0 -0
  17. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/cli/__init__.py +0 -0
  18. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/cli/cli.py +0 -0
  19. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/cli/drill_down.py +0 -0
  20. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/csvpath.py +0 -0
  21. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/csvpaths.py +0 -0
  22. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/__init__.py +0 -0
  23. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/files/file_cacher.py +0 -0
  24. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/files/file_manager.py +0 -0
  25. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/files/file_metadata.py +0 -0
  26. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/files/file_registrar.py +0 -0
  27. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ckan/ckan.py +0 -0
  28. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ckan/ckan_listener.py +0 -0
  29. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ckan/datafile.py +0 -0
  30. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ckan/dataset.py +0 -0
  31. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/event.py +0 -0
  32. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/event_result.py +0 -0
  33. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/file_listener_ol.py +0 -0
  34. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/job.py +0 -0
  35. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/ol_listener.py +0 -0
  36. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/paths_listener_ol.py +0 -0
  37. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/result_listener_ol.py +0 -0
  38. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/results_listener_ol.py +0 -0
  39. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/run.py +0 -0
  40. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/run_listener_ol.py +0 -0
  41. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/run_state.py +0 -0
  42. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/ol/sender.py +0 -0
  43. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/slack/event.py +0 -0
  44. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/integrations/slack/sender.py +0 -0
  45. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/listener.py +0 -0
  46. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/metadata.py +0 -0
  47. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/paths/paths_metadata.py +0 -0
  48. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/paths/paths_registrar.py +0 -0
  49. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/registrar.py +0 -0
  50. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/file_errors_reader.py +0 -0
  51. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/file_lines_reader.py +0 -0
  52. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/file_printouts_reader.py +0 -0
  53. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/file_unmatched_reader.py +0 -0
  54. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/readers/readers.py +0 -0
  55. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result.py +0 -0
  56. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result_file_reader.py +0 -0
  57. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result_metadata.py +0 -0
  58. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/result_registrar.py +0 -0
  59. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/results/results_metadata.py +0 -0
  60. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/run/run_listener_stdout.py +0 -0
  61. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/run/run_metadata.py +0 -0
  62. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/managers/run/run_registrar.py +0 -0
  63. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/__init__.py +0 -0
  64. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/__init__.py +0 -0
  65. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/args.py +0 -0
  66. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/all.py +0 -0
  67. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/andf.py +0 -0
  68. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/any.py +0 -0
  69. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/between.py +0 -0
  70. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/empty.py +0 -0
  71. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/exists.py +0 -0
  72. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/inf.py +0 -0
  73. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/no.py +0 -0
  74. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/notf.py +0 -0
  75. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/orf.py +0 -0
  76. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/boolean/yes.py +0 -0
  77. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count.py +0 -0
  78. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count_bytes.py +0 -0
  79. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count_headers.py +0 -0
  80. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count_lines.py +0 -0
  81. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/count_scans.py +0 -0
  82. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/counter.py +0 -0
  83. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/every.py +0 -0
  84. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/has_matches.py +0 -0
  85. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/increment.py +0 -0
  86. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/tally.py +0 -0
  87. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/counting/total_lines.py +0 -0
  88. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/dates/now.py +0 -0
  89. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/function.py +0 -0
  90. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/function_factory.py +0 -0
  91. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/function_finder.py +0 -0
  92. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/function_focus.py +0 -0
  93. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/append.py +0 -0
  94. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/collect.py +0 -0
  95. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/empty_stack.py +0 -0
  96. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/end.py +0 -0
  97. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/header_name.py +0 -0
  98. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/header_names_mismatch.py +0 -0
  99. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/headers.py +0 -0
  100. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/mismatch.py +0 -0
  101. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/replace.py +0 -0
  102. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/headers/reset_headers.py +0 -0
  103. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/advance.py +0 -0
  104. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/after_blank.py +0 -0
  105. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/dups.py +0 -0
  106. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/first.py +0 -0
  107. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/first_line.py +0 -0
  108. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/last.py +0 -0
  109. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/lines/stop.py +0 -0
  110. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/above.py +0 -0
  111. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/add.py +0 -0
  112. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/divide.py +0 -0
  113. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/equals.py +0 -0
  114. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/intf.py +0 -0
  115. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/mod.py +0 -0
  116. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/multiply.py +0 -0
  117. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/round.py +0 -0
  118. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/subtotal.py +0 -0
  119. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/subtract.py +0 -0
  120. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/math/sum.py +0 -0
  121. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/misc/fingerprint.py +0 -0
  122. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/misc/importf.py +0 -0
  123. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/misc/random.py +0 -0
  124. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/jinjaf.py +0 -0
  125. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/print_line.py +0 -0
  126. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/print_queue.py +0 -0
  127. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/printf.py +0 -0
  128. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/print/table.py +0 -0
  129. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/stats/minf.py +0 -0
  130. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/stats/percent.py +0 -0
  131. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/stats/percent_unique.py +0 -0
  132. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/stats/stdev.py +0 -0
  133. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/concat.py +0 -0
  134. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/length.py +0 -0
  135. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/lower.py +0 -0
  136. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/metaphone.py +0 -0
  137. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/regex.py +0 -0
  138. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/starts_with.py +0 -0
  139. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/strip.py +0 -0
  140. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/substring.py +0 -0
  141. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/strings/upper.py +0 -0
  142. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/testing/debug.py +0 -0
  143. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/__init__.py +0 -0
  144. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/boolean.py +0 -0
  145. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/datef.py +0 -0
  146. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/decimal.py +0 -0
  147. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/nonef.py +0 -0
  148. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/string.py +0 -0
  149. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/types/type.py +0 -0
  150. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/validity/fail.py +0 -0
  151. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/validity/failed.py +0 -0
  152. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/validity/line.py +0 -0
  153. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/get.py +0 -0
  154. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/pushpop.py +0 -0
  155. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/put.py +0 -0
  156. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/track.py +0 -0
  157. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/functions/variables/variables.py +0 -0
  158. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/lark_parser.py +0 -0
  159. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/lark_transformer.py +0 -0
  160. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/matcher.py +0 -0
  161. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/__init__.py +0 -0
  162. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/equality.py +0 -0
  163. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/expression.py +0 -0
  164. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/header.py +0 -0
  165. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/matchable.py +0 -0
  166. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/qualified.py +0 -0
  167. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/reference.py +0 -0
  168. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/term.py +0 -0
  169. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/productions/variable.py +0 -0
  170. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/exceptions.py +0 -0
  171. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/expression_encoder.py +0 -0
  172. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/expression_utility.py +0 -0
  173. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/lark_print_parser.py +0 -0
  174. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/print_parser.py +0 -0
  175. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/matching/util/runtime_data_collector.py +0 -0
  176. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/explain_mode.py +0 -0
  177. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/files_mode.py +0 -0
  178. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/logic_mode.py +0 -0
  179. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/mode_controller.py +0 -0
  180. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/print_mode.py +0 -0
  181. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/return_mode.py +0 -0
  182. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/run_mode.py +0 -0
  183. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/source_mode.py +0 -0
  184. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/transfer_mode.py +0 -0
  185. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/unmatched_mode.py +0 -0
  186. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/modes/validation_mode.py +0 -0
  187. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/__init__.py +0 -0
  188. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/exceptions.py +0 -0
  189. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/parser.out +0 -0
  190. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/parsetab.py +0 -0
  191. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/scanner.py +0 -0
  192. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/scanning/scanning_lexer.py +0 -0
  193. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/cache.py +0 -0
  194. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/class_loader.py +0 -0
  195. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/config_exception.py +0 -0
  196. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/error.py +0 -0
  197. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/exceptions.py +0 -0
  198. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/file_info.py +0 -0
  199. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/file_readers.py +0 -0
  200. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/file_writers.py +0 -0
  201. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/last_line_stats.py +0 -0
  202. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/line_counter.py +0 -0
  203. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/line_monitor.py +0 -0
  204. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/line_spooler.py +0 -0
  205. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/log_utility.py +0 -0
  206. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/metadata_parser.py +0 -0
  207. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/nos.py +0 -0
  208. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/pandas_data_reader.py +0 -0
  209. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/printer.py +0 -0
  210. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/reference_parser.py +0 -0
  211. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_data_reader.py +0 -0
  212. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_data_writer.py +0 -0
  213. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_fingerprinter.py +0 -0
  214. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_utils.py +0 -0
  215. {csvpath-0.0.504 → csvpath-0.0.506}/csvpath/util/s3/s3_xlsx_data_reader.py +0 -0
  216. {csvpath-0.0.504 → csvpath-0.0.506}/docs/asbool.md +0 -0
  217. {csvpath-0.0.504 → csvpath-0.0.506}/docs/assignment.md +0 -0
  218. {csvpath-0.0.504 → csvpath-0.0.506}/docs/comments.md +0 -0
  219. {csvpath-0.0.504 → csvpath-0.0.506}/docs/config.md +0 -0
  220. {csvpath-0.0.504 → csvpath-0.0.506}/docs/examples.md +0 -0
  221. {csvpath-0.0.504 → csvpath-0.0.506}/docs/files.md +0 -0
  222. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/above.md +0 -0
  223. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/advance.md +0 -0
  224. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/after_blank.md +0 -0
  225. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/all.md +0 -0
  226. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/andor.md +0 -0
  227. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/any.md +0 -0
  228. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/average.md +0 -0
  229. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/between.md +0 -0
  230. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/collect.md +0 -0
  231. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/correlate.md +0 -0
  232. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/count.md +0 -0
  233. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/count_bytes.md +0 -0
  234. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/count_headers.md +0 -0
  235. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/counter.md +0 -0
  236. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/date.md +0 -0
  237. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/empty.md +0 -0
  238. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/empty_stack.md +0 -0
  239. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/end.md +0 -0
  240. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/every.md +0 -0
  241. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/fail.md +0 -0
  242. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/fingerprint.md +0 -0
  243. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/first.md +0 -0
  244. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/get.md +0 -0
  245. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/has_dups.md +0 -0
  246. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/has_matches.md +0 -0
  247. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/header.md +0 -0
  248. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/header_name.md +0 -0
  249. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/header_names_mismatch.md +0 -0
  250. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/implementing_functions.md +0 -0
  251. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/import.md +0 -0
  252. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/in.md +0 -0
  253. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/increment.md +0 -0
  254. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/intf.md +0 -0
  255. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/jinja.md +0 -0
  256. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/last.md +0 -0
  257. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/line.md +0 -0
  258. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/line_number.md +0 -0
  259. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/max.md +0 -0
  260. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/metaphone.md +0 -0
  261. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/mismatch.md +0 -0
  262. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/no.md +0 -0
  263. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/not.md +0 -0
  264. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/now.md +0 -0
  265. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/percent_unique.md +0 -0
  266. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/pop.md +0 -0
  267. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/print.md +0 -0
  268. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/print_line.md +0 -0
  269. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/print_queue.md +0 -0
  270. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/random.md +0 -0
  271. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/regex.md +0 -0
  272. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/replace.md +0 -0
  273. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/reset_headers.md +0 -0
  274. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/stdev.md +0 -0
  275. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/stop.md +0 -0
  276. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/string_functions.md +0 -0
  277. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/subtotal.md +0 -0
  278. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/subtract.md +0 -0
  279. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/sum.md +0 -0
  280. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/tally.md +0 -0
  281. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/total_lines.md +0 -0
  282. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/track.md +0 -0
  283. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/types.md +0 -0
  284. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/variables.md +0 -0
  285. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions/variables_and_headers.md +0 -0
  286. {csvpath-0.0.504 → csvpath-0.0.506}/docs/functions.md +0 -0
  287. {csvpath-0.0.504 → csvpath-0.0.506}/docs/grammar.md +0 -0
  288. {csvpath-0.0.504 → csvpath-0.0.506}/docs/headers.md +0 -0
  289. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/ckan-logo-sm.png +0 -0
  290. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/csvpath-icon-sm.png +0 -0
  291. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/csvpath-logo-wordmark-tight-2.svg +0 -0
  292. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/logo-wordmark-3.svg +0 -0
  293. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/logo-wordmark-4.svg +0 -0
  294. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/logo-wordmark-white-on-black-trimmed-padded.png +0 -0
  295. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/logo-wordmark-white-trimmed.png +0 -0
  296. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/marquez-logo-sm.png +0 -0
  297. {csvpath-0.0.504 → csvpath-0.0.506}/docs/images/openlineage-logo-sm.png +0 -0
  298. {csvpath-0.0.504 → csvpath-0.0.506}/docs/paths.md +0 -0
  299. {csvpath-0.0.504 → csvpath-0.0.506}/docs/printing.md +0 -0
  300. {csvpath-0.0.504 → csvpath-0.0.506}/docs/qualifiers.md +0 -0
  301. {csvpath-0.0.504 → csvpath-0.0.506}/docs/references.md +0 -0
  302. {csvpath-0.0.504 → csvpath-0.0.506}/docs/terms.md +0 -0
  303. {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.504
4
- Summary: A declarative language for validating CSV, Excel, and other tabular data files
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 local Marquez
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).exists():
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
- with DataFileWriter(path=os.path.join(home, "definition.json")) as writer:
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