eco-helpers 2.0.16 → 2.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -6
  3. data/eco-helpers.gemspec +6 -4
  4. data/lib/eco-helpers.rb +1 -0
  5. data/lib/eco/api/common/base_loader.rb +14 -0
  6. data/lib/eco/api/common/people/default_parsers/date_parser.rb +11 -1
  7. data/lib/eco/api/common/people/default_parsers/login_providers_parser.rb +1 -1
  8. data/lib/eco/api/common/people/default_parsers/policy_groups_parser.rb +11 -11
  9. data/lib/eco/api/common/people/person_entry.rb +4 -2
  10. data/lib/eco/api/common/session/mailer.rb +0 -1
  11. data/lib/eco/api/common/session/s3_uploader.rb +0 -1
  12. data/lib/eco/api/common/session/sftp.rb +0 -1
  13. data/lib/eco/api/microcases.rb +3 -1
  14. data/lib/eco/api/microcases/append_usergroups.rb +0 -1
  15. data/lib/eco/api/microcases/people_cache.rb +2 -2
  16. data/lib/eco/api/microcases/people_load.rb +2 -2
  17. data/lib/eco/api/microcases/people_refresh.rb +2 -2
  18. data/lib/eco/api/microcases/people_search.rb +6 -6
  19. data/lib/eco/api/microcases/preserve_default_tag.rb +23 -0
  20. data/lib/eco/api/microcases/preserve_filter_tags.rb +28 -0
  21. data/lib/eco/api/microcases/preserve_policy_groups.rb +30 -0
  22. data/lib/eco/api/microcases/set_account.rb +0 -1
  23. data/lib/eco/api/organization.rb +1 -0
  24. data/lib/eco/api/organization/people.rb +7 -0
  25. data/lib/eco/api/organization/people_analytics.rb +60 -0
  26. data/lib/eco/api/organization/presets_factory.rb +22 -83
  27. data/lib/eco/api/organization/presets_integrity.json +6 -0
  28. data/lib/eco/api/organization/presets_values.json +5 -4
  29. data/lib/eco/api/policies/default_policies/99_user_access_policy.rb +0 -30
  30. data/lib/eco/api/session.rb +1 -20
  31. data/lib/eco/api/session/batch.rb +23 -7
  32. data/lib/eco/api/session/config.rb +0 -10
  33. data/lib/eco/api/session/config/people.rb +1 -17
  34. data/lib/eco/api/usecases/default_cases.rb +1 -1
  35. data/lib/eco/api/usecases/default_cases/abstract_policygroup_abilities_case.rb +1 -1
  36. data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +76 -0
  37. data/lib/eco/api/usecases/default_cases/codes_to_tags_case.rb +2 -3
  38. data/lib/eco/api/usecases/default_cases/reset_landing_page_case.rb +11 -1
  39. data/lib/eco/api/usecases/default_cases/restore_db_case.rb +1 -2
  40. data/lib/eco/api/usecases/default_cases/supers_cyclic_identify_case.rb +1 -1
  41. data/lib/eco/api/usecases/default_cases/supers_hierarchy_case.rb +1 -1
  42. data/lib/eco/api/usecases/default_cases/to_csv_case.rb +85 -27
  43. data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +62 -36
  44. data/lib/eco/cli/config/default/options.rb +19 -17
  45. data/lib/eco/cli/config/default/people_filters.rb +3 -3
  46. data/lib/eco/cli/config/default/usecases.rb +66 -32
  47. data/lib/eco/cli/config/default/workflow.rb +1 -1
  48. data/lib/eco/cli/config/help.rb +1 -0
  49. data/lib/eco/cli/config/options_set.rb +106 -13
  50. data/lib/eco/cli/config/use_cases.rb +33 -33
  51. data/lib/eco/cli/scripting/args_helpers.rb +30 -3
  52. data/lib/eco/data.rb +1 -0
  53. data/lib/eco/data/crypto/encryption.rb +3 -3
  54. data/lib/eco/data/files/helpers.rb +6 -4
  55. data/lib/eco/data/fuzzy_match.rb +119 -0
  56. data/lib/eco/data/fuzzy_match/array_helpers.rb +75 -0
  57. data/lib/eco/data/fuzzy_match/chars_position_score.rb +37 -0
  58. data/lib/eco/data/fuzzy_match/ngrams_score.rb +73 -0
  59. data/lib/eco/data/fuzzy_match/pairing.rb +102 -0
  60. data/lib/eco/data/fuzzy_match/result.rb +67 -0
  61. data/lib/eco/data/fuzzy_match/results.rb +53 -0
  62. data/lib/eco/data/fuzzy_match/score.rb +44 -0
  63. data/lib/eco/data/fuzzy_match/stop_words.rb +35 -0
  64. data/lib/eco/data/fuzzy_match/string_helpers.rb +69 -0
  65. data/lib/eco/version.rb +1 -1
  66. metadata +82 -10
  67. data/lib/eco/api/microcases/refresh_abilities.rb +0 -19
  68. data/lib/eco/api/organization/presets_reference.json +0 -59
  69. data/lib/eco/api/usecases/default_cases/refresh_abilities_case.rb +0 -30
@@ -5,18 +5,35 @@ module Eco
5
5
  include Eco::CLI::Config::Help
6
6
  attr_reader :core_config
7
7
 
8
+ class CaseConfig < Struct.new(:cases_config, :option, :type, :description, :casename, :callback)
9
+
10
+ def add_option(arg, desc = nil, &block)
11
+ core_config.options_set.add(arg, desc, namespace: option, &block)
12
+ self
13
+ end
14
+
15
+ private
16
+
17
+ def core_config
18
+ cases_config.core_config
19
+ end
20
+ end
21
+
22
+ class ActiveCase < Struct.new(:index, :option, :callback)
23
+
24
+ end
25
+
8
26
  def initialize(core_config:)
9
27
  @core_config = core_config
10
28
  @linked_cases = {}
11
- @description = {}
12
29
  end
13
30
 
14
31
  # @return [String] summary of the use cases.
15
32
  def help
16
33
  ["The following are the available use cases:"].yield_self do |lines|
17
34
  max_len = keys_max_len(@linked_cases.keys)
18
- @linked_cases.keys.sort.each do |key|
19
- lines << help_line(key, @description[key], max_len)
35
+ @linked_cases.keys.sort.each do |option_case|
36
+ lines << help_line(option_case, @linked_cases[option_case].description, max_len)
20
37
  end
21
38
  lines
22
39
  end.join("\n")
@@ -33,18 +50,8 @@ module Eco
33
50
  raise "You must specify a valid 'case_name' when no block is provided" unless case_name
34
51
  raise "'case_name' expected to be a String. Given: #{case_name.class}" unless case_name.is_a?(String)
35
52
  end
36
-
37
- @linked_cases[option_case] = {
38
- type => {
39
- option: option_case,
40
- type: type,
41
- casename: case_name,
42
- callback: callback
43
- }
44
- }
45
- @description[option_case] = desc
46
-
47
- self
53
+ puts "Overriding case config '#{option_case}'" if @linked_cases.key?(option_case)
54
+ @linked_cases[option_case] = CaseConfig.new(self, option_case, type, desc, case_name, callback)
48
55
  end
49
56
 
50
57
  # Scopes/identifies which usecases are being invoked from the command line
@@ -55,20 +62,13 @@ module Eco
55
62
  def active(io:)
56
63
  validate_io!(io)
57
64
  return @active_cases unless !@active_cases
58
- active_cases = {}
59
- @linked_cases.each do |option_case, types|
65
+ @active_cases = @linked_cases.each_with_object({}) do |(option_case, data), active_cases|
60
66
  next nil unless SCR.get_arg(option_case)
61
- types.each do |type, data|
62
- if usecase = get_usecase(io: io, data: data)
63
- active_cases[usecase] = {
64
- index: SCR.get_arg_index(option_case),
65
- option: option_case,
66
- callback: data[:callback]
67
- }
68
- end
67
+ if usecase = get_usecase(io: io, data: data)
68
+ index = SCR.get_arg_index(option_case)
69
+ active_cases[usecase] = ActiveCase.new(index, option_case, data.callback)
69
70
  end
70
- end
71
- @active_cases = active_cases.sort_by {|c, d| d[:index]}.to_h
71
+ end.sort_by {|c, d| d.index}.to_h
72
72
  end
73
73
 
74
74
  def process(io:)
@@ -79,7 +79,7 @@ module Eco
79
79
  processed = true
80
80
  io = case_io(io: io, usecase: usecase)
81
81
  # some usecases have a callback to collect the parameters
82
- data[:callback]&.call(*io.params)
82
+ data.callback&.call(*io.params)
83
83
  io = usecase.launch(io: io)
84
84
  end
85
85
  processed
@@ -100,17 +100,17 @@ module Eco
100
100
  end
101
101
 
102
102
  def get_usecase(io:, data:)
103
- usecase = if case_name = data[:casename]
104
- io.session.usecases.case(case_name, type: data[:type])
103
+ usecase = if case_name = data.casename
104
+ io.session.usecases.case(case_name, type: data.type)
105
105
  end
106
- usecase ||= if callback = data[:callback]
106
+ usecase ||= if callback = data.callback
107
107
  # identify/retrieve usecase via callback
108
- params = io.params(keyed: true).merge(type: data[:type])
108
+ params = io.params(keyed: true).merge(type: data.type)
109
109
  io = io.new(**params, validate: false)
110
110
  callback.call(*io.params).tap do |usecase|
111
111
  unless usecase.is_a?(Eco::API::UseCases::UseCase)
112
112
  msg = "When adding a usecase, without specifying 'case_name:', "
113
- msg += "the block that integrates usecase for cli option '#{data[:option]}'"
113
+ msg += "the block that integrates usecase for cli option '#{data.option}'"
114
114
  msg += " must return an Eco::API::UseCases::UseCase object. It returns #{usecase.class}"
115
115
  raise msg
116
116
  end
@@ -3,6 +3,7 @@ module Eco
3
3
  class Scripting
4
4
  module ArgsHelpers
5
5
 
6
+ # @return [Array<String] the command line arguments.
6
7
  def argv
7
8
  @argv || ARGV
8
9
  end
@@ -11,10 +12,18 @@ module Eco
11
12
  Argument.is_modifier?(value)
12
13
  end
13
14
 
15
+ # @return [Arguments] supported known arguments.
14
16
  def arguments
15
17
  @arguments ||= Arguments.new(argv)
16
18
  end
17
19
 
20
+ # Registers an argument as a known one.
21
+ def known_argument(key, with_param: false)
22
+ arguments.add(key, with_param: with_param)
23
+ end
24
+
25
+
26
+ # Validation to stop the `script` if among `argv` there's any **unknown** argument.
18
27
  def stop_on_unknown!(exclude: [], only_options: false)
19
28
  # validate only those that are options
20
29
  unknown = arguments.unknown(exclude: exclude)
@@ -23,18 +32,35 @@ module Eco
23
32
  end
24
33
 
25
34
  unless unknown.empty?
26
- raise "There are unknown options in your command line arguments: #{unknown}"
35
+ msg = "There are unknown options in your command line arguments:\n"
36
+ msg += "#{unknown}\n"
37
+ msg += "Please, remember that use case specific options should come after the use case in the command line.\n"
38
+ msg += "Use 'ruby main.rb -org [-usecase] --help -options' for more information"
39
+ raise msg
27
40
  end
28
41
  end
29
42
 
43
+ # @return [Boolean] if `key` is in the command line.
44
+ def arg?(key)
45
+ argv.include?(key)
46
+ end
47
+
48
+ # @return [Integer, nil] the position of `key` in the command line.
30
49
  def get_arg_index(key)
31
- return nil if !argv.include?(key)
50
+ return nil if !arg?(key)
32
51
  argv.index(key)
33
52
  end
34
53
 
54
+ # @return [Boolean] if `key1` precedes `key2` in the command line.
55
+ def arg_order?(key1, key2)
56
+ return false unless (k1 = get_arg_index(key1)) && k2 = get_arg_index(key2)
57
+ k1 < k2
58
+ end
59
+
60
+ # @return [String, Boolean] the argument value if `with_param` or a `Boolean` if not.
35
61
  def get_arg(key, with_param: false, valid: true)
36
62
  # track what a known option looks like
37
- arguments.add(key, with_param: with_param)
63
+ known_argument(key, with_param: with_param)
38
64
  return nil unless index = get_arg_index(key)
39
65
  value = true
40
66
  if with_param
@@ -45,6 +71,7 @@ module Eco
45
71
  return value
46
72
  end
47
73
 
74
+ # @return [String] the filename.
48
75
  def get_file(key, required: false, should_exist: true)
49
76
  filename = get_arg(key, with_param: true)
50
77
  if !filename && required
data/lib/eco/data.rb CHANGED
@@ -6,3 +6,4 @@ end
6
6
  require_relative 'data/crypto'
7
7
  require_relative 'data/files'
8
8
  require_relative 'data/mapper'
9
+ require_relative 'data/fuzzy_match'
@@ -1,7 +1,7 @@
1
1
  require 'openssl'
2
- require 'json'
2
+ # 'json'
3
3
  require 'base64'
4
- require 'pp'
4
+ #require 'pp'
5
5
 
6
6
  require_relative '../../cli/scripting'
7
7
 
@@ -147,7 +147,7 @@ module Eco
147
147
  return str_c
148
148
  #EncryptedData.new({content: str_c, key: key, iv: iv})
149
149
 
150
-
150
+
151
151
  end
152
152
  def aes256_decrypt(data, key: , iv: , block_octets: BLOCK_OCTETS)
153
153
  block_bits = block_bits * 8
@@ -3,11 +3,13 @@ module Eco
3
3
  module Files
4
4
  DEFAULT_TIMESTAMP_PATTERN = '%Y-%m-%dT%H%M%S'
5
5
 
6
- def self.included(base)
7
- base.send(:include, InstanceMethods)
8
- base.extend(ClassMethods)
6
+ class << self
7
+ def included(base)
8
+ base.send(:include, InstanceMethods)
9
+ base.extend(ClassMethods)
10
+ end
9
11
  end
10
-
12
+
11
13
  module InstanceMethods
12
14
 
13
15
  end
@@ -0,0 +1,119 @@
1
+ require 'fuzzy_match'
2
+ require 'amatch'
3
+ require 'jaro_winkler'
4
+
5
+ require_relative 'fuzzy_match/stop_words'
6
+ require_relative 'fuzzy_match/array_helpers'
7
+ require_relative 'fuzzy_match/string_helpers'
8
+ require_relative 'fuzzy_match/pairing'
9
+ require_relative 'fuzzy_match/chars_position_score'
10
+ require_relative 'fuzzy_match/ngrams_score'
11
+
12
+ module Eco
13
+ module Data
14
+ module FuzzyMatch
15
+
16
+ class << self
17
+ def included(base)
18
+ base.send(:include, InstanceMethods)
19
+ base.extend(ClassMethods)
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ include ArrayHelpers
25
+ include StringHelpers
26
+ include Pairing
27
+ include CharsPositionScore
28
+ include NGramsScore
29
+
30
+ def jaro_winkler(str1, str2)
31
+ options = {
32
+ ignore_case: true,
33
+ weight: 0.25
34
+ }
35
+ JaroWinkler.distance(str1, str2, **options)
36
+ end
37
+
38
+ end
39
+
40
+ module InstanceMethods
41
+ include StopWords
42
+
43
+ attr_accessor :fuzzy_options
44
+
45
+ def fuzzy_options
46
+ @fuzzy_options ||= {}
47
+ end
48
+
49
+ def fuzzy_match(haystack = nil, **options)
50
+ return @fuzzy_match if instance_variable_defined?(:@fuzzy_match)
51
+ @fuzzy_options = options.merge({
52
+ stop_words: PREPOSITIONS + PRONOUNS + ARTICLES
53
+ })
54
+ # make it run with a native C extension (for better performance: ~130 % increase of performance)
55
+ ::FuzzyMatch.engine = :amatch
56
+ haystack = obtain_haystack(haystack).tap do |items|
57
+ if !fuzzy_read_method && found = items.find {|item| !item.is_a?(String)}
58
+ raise "To use non String objects as 'haystack' you should provide `read:` or `options[:read]`. Given element: #{found.class}"
59
+ end
60
+ end
61
+ @fuzzy_match = ::FuzzyMatch.new(haystack, fuzzy_options)
62
+ end
63
+
64
+ # @note
65
+ # - When the `haystack` elements are **non** `String` objects, it excludes the needle itself from the results
66
+ # @param needle [String, Object] object is allowed when `fuzzy_options` includes `read:` key
67
+ # @return [Eco::Data::FuzzyMatch::Results]
68
+ def find_all_with_score(needle, **options)
69
+ results = fuzzy_match(**options).find_all_with_score(needle).each_with_object([]) do |fuzzy_results, results|
70
+ item, dice, lev = fuzzy_results
71
+ unless item == needle
72
+ needle_str = item_string(needle)
73
+ item_str = item_string(item)
74
+ jaro_res = self.class.jaro_winkler(needle_str, item_str)
75
+ ngram_res = self.class.ngrams_score(needle_str, item_str, range: 3..5).ratio
76
+ wngram_res = self.class.words_ngrams_score(needle_str, item_str, range: 3..7).ratio
77
+ pos_res = self.class.chars_position_score(needle_str, item_str).ratio
78
+ results << Result.new(item, item_str, dice, lev, jaro_res, ngram_res, wngram_res, pos_res)
79
+ end
80
+ end
81
+ Results.new(needle, item_string(needle), results)
82
+ end
83
+
84
+ private
85
+
86
+ # @note
87
+ # - When used in an `Enumerable` it will use `to_a`, or `values` if it's a `Hash`
88
+ # @param data [Enumerable, nil]
89
+ # @return [Array<Object>] the non-repeated values of `data`
90
+ def obtain_haystack(data = nil)
91
+ data = self if self.is_a?(Enumerable) && !data
92
+ raise "'data' should be an Enumerable. Given: #{data.class}" unless data.is_a?(Enumerable)
93
+ data = self.is_a?(Hash) ? self.values.flatten : to_a.flatten
94
+ data.uniq.compact
95
+ end
96
+
97
+ def item_string(item, attr = fuzzy_read_method)
98
+ return item if !item || item.is_a?(String) || !attr
99
+ attr = attr.to_sym
100
+ return item.send(attr) if item.respond_to?(attr)
101
+ end
102
+
103
+ def fuzzy_read_method
104
+ fuzzy_options[:read]
105
+ end
106
+
107
+ end
108
+
109
+ class << self
110
+ include FuzzyMatch::ClassMethods
111
+ end
112
+
113
+ end
114
+ end
115
+ end
116
+
117
+ require_relative 'fuzzy_match/score'
118
+ require_relative 'fuzzy_match/result'
119
+ require_relative 'fuzzy_match/results'
@@ -0,0 +1,75 @@
1
+ module Eco
2
+ module Data
3
+ module FuzzyMatch
4
+ module ArrayHelpers
5
+ # Keeps the start order of the `values` and consecutive `values` together/consecutive.
6
+ # @param values [Array] the input array with the values.
7
+ # @param range [Integer, Range] determine the lenght of the generated values.
8
+ # @return [Array<Array<Value>>] combinations of `range` length of `values`.
9
+ def ngrams(values, range=2..3)
10
+ [].tap do |out|
11
+ if range.is_a?(Integer)
12
+ n = range
13
+ values_count = values.length
14
+ values.each_with_index do |word, i|
15
+ min = i
16
+ max = i + (n - 1)
17
+ break if values_count <= max
18
+ out << values[min..max].join(' ')
19
+ end
20
+ out.uniq!
21
+ else
22
+ range.each {|n| out.concat(ngrams(values, n))}
23
+ out.uniq!
24
+ end
25
+ end
26
+ end
27
+
28
+ # Keeps the start order of the `values` of the input `Array` `values`.
29
+ # It does **not** keep consecutive `values` together (it can jump/skip items).
30
+ # @param values [Array] the input array with the values.
31
+ # @param range [Integer, Range] determine the lenght of the generated values.
32
+ # @return [Array<Array<Value>>] combinations of `range` length of `values`
33
+ def combinations(values, range=2..3)
34
+ if range.is_a?(Integer)
35
+ values.combination(range).to_a
36
+ else
37
+ range.flat_map {|size| values.combination(size).to_a}
38
+ end
39
+ end
40
+
41
+ # It includes `combinations` that break the initial order of the `Array`.
42
+ # It does **not** keep consecutive `values` together (it can jump/skip items).
43
+ # @param values [Array] the input array with the values.
44
+ # @param range [Integer, Range] determine the lenght of the generated values.
45
+ # @return [Array<Array<Value>>] permutations of `range` length of `values`
46
+ def permutations(values, range=2..3)
47
+ combinations(values, range).tap do |out|
48
+ range = range.is_a?(Integer)? (range..range) : range
49
+ out.dup.select do |item|
50
+ range.include?(item.length)
51
+ end.each do |comb|
52
+ comb.permutation.to_a.tap do |perms|
53
+ perms.each {|perm| out << perm}
54
+ end
55
+ end
56
+ out.uniq!
57
+ end
58
+ end
59
+
60
+ # Helper to praper facet structure
61
+ # @param values1 [Array] the input array with the values to have their facet against.
62
+ # @param values2 [Array] the input array with the values to facet against.
63
+ # @return [Hash] where `keys` are `values1` and `value` of each `key` all `values2`
64
+ def facet(values1, values2)
65
+ {}.tap do |out|
66
+ next unless values1.is_a?(Enumerable)
67
+ values1 = values1.is_a?(Hash) ? values1.values : values1.to_a
68
+ values1.each {|val| out[val] = values2.dup}
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,37 @@
1
+ module Eco
2
+ module Data
3
+ module FuzzyMatch
4
+ module CharsPositionScore
5
+ # For each character in `str1`, a search is performed on `str2`.
6
+ # The search is deemed successful if a character is found in `str2` within `max_distance` characters of the current position.
7
+ # A score is kept of matching characters.
8
+ # @note This algorithm is best suited for matching mis-spellings.
9
+ # @max_distance [Integer] maximum char position distance to score.
10
+ # @normalized [Boolean] to avoid double ups in normalizing.
11
+ # @return [Score] the score object with the result.
12
+ def chars_position_score(str1, str2, max_distance: 3, normalized: false)
13
+ str1, str2 = normalize_string([str1, str2]) unless normalized
14
+ len1 = str1 && str1.length; len2 = str2 && str2.length
15
+ Score.new(0, len1 || 0).tap do |score|
16
+ next if !str1 || !str2
17
+ next score.increase(score.total) if str1 == str2
18
+ next if len1 < 2
19
+ pos = 0
20
+ len1.times do |i|
21
+ start = pos + 1
22
+ found = false
23
+ if pos = str2.index(str1[i])
24
+ if pos < (start + max_distance)
25
+ found = true
26
+ score.increase
27
+ end
28
+ end
29
+ pos = start unless found
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end