gooddata 0.6.11 → 0.6.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.travis.yml +5 -0
  4. data/CHANGELOG.md +34 -1
  5. data/CLI.md +1 -1
  6. data/authors.sh +4 -0
  7. data/lib/gooddata.rb +1 -1
  8. data/lib/gooddata/cli/commands/api_cmd.rb +0 -2
  9. data/lib/gooddata/cli/commands/auth_cmd.rb +0 -3
  10. data/lib/gooddata/cli/commands/console_cmd.rb +1 -2
  11. data/lib/gooddata/cli/commands/domain_cmd.rb +0 -2
  12. data/lib/gooddata/cli/commands/process_cmd.rb +0 -2
  13. data/lib/gooddata/cli/commands/project_cmd.rb +0 -2
  14. data/lib/gooddata/cli/commands/projects_cmd.rb +0 -2
  15. data/lib/gooddata/cli/commands/run_ruby_cmd.rb +2 -3
  16. data/lib/gooddata/cli/commands/scaffold_cmd.rb +0 -3
  17. data/lib/gooddata/cli/commands/user_cmd.rb +0 -2
  18. data/lib/gooddata/cli/shared.rb +1 -2
  19. data/lib/gooddata/commands/datawarehouse.rb +24 -0
  20. data/lib/gooddata/commands/process.rb +0 -1
  21. data/lib/gooddata/commands/project.rb +1 -1
  22. data/lib/gooddata/commands/scaffold.rb +0 -1
  23. data/lib/gooddata/core/connection.rb +376 -0
  24. data/lib/gooddata/core/logging.rb +13 -0
  25. data/lib/gooddata/core/rest.rb +40 -16
  26. data/lib/gooddata/exceptions/user_in_different_domain.rb +11 -0
  27. data/lib/gooddata/extensions/enumerable.rb +8 -0
  28. data/lib/gooddata/goodzilla/goodzilla.rb +24 -0
  29. data/lib/gooddata/helpers/global_helpers.rb +126 -12
  30. data/lib/gooddata/mixins/author.rb +11 -5
  31. data/lib/gooddata/mixins/is_dimension.rb +13 -0
  32. data/lib/gooddata/mixins/md_object_indexer.rb +17 -1
  33. data/lib/gooddata/mixins/md_object_query.rb +10 -2
  34. data/lib/gooddata/mixins/md_relations.rb +2 -2
  35. data/lib/gooddata/mixins/rest_resource.rb +1 -0
  36. data/lib/gooddata/models/data_result.rb +0 -1
  37. data/lib/gooddata/models/datawarehouse.rb +90 -0
  38. data/lib/gooddata/models/domain.rb +202 -76
  39. data/lib/gooddata/models/execution.rb +11 -0
  40. data/lib/gooddata/models/from_wire.rb +4 -4
  41. data/lib/gooddata/models/invitation.rb +0 -5
  42. data/lib/gooddata/models/membership.rb +121 -91
  43. data/lib/gooddata/models/metadata.rb +1 -2
  44. data/lib/gooddata/models/metadata/attribute.rb +7 -0
  45. data/lib/gooddata/models/metadata/dashboard.rb +1 -1
  46. data/lib/gooddata/models/metadata/dimension.rb +52 -0
  47. data/lib/gooddata/models/metadata/fact.rb +1 -1
  48. data/lib/gooddata/models/metadata/label.rb +21 -7
  49. data/lib/gooddata/models/metadata/metric.rb +1 -23
  50. data/lib/gooddata/models/metadata/report.rb +2 -2
  51. data/lib/gooddata/models/metadata/report_definition.rb +22 -2
  52. data/lib/gooddata/models/metadata/variable.rb +81 -0
  53. data/lib/gooddata/models/model.rb +2 -1
  54. data/lib/gooddata/models/process.rb +3 -4
  55. data/lib/gooddata/models/profile.rb +50 -82
  56. data/lib/gooddata/models/project.rb +170 -213
  57. data/lib/gooddata/models/project_blueprint.rb +14 -5
  58. data/lib/gooddata/models/project_creator.rb +2 -2
  59. data/lib/gooddata/models/schedule.rb +10 -8
  60. data/lib/gooddata/models/to_wire.rb +2 -2
  61. data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +67 -0
  62. data/lib/gooddata/models/user_filters/user_filter.rb +96 -0
  63. data/lib/gooddata/models/user_filters/user_filter_builder.rb +409 -0
  64. data/lib/gooddata/{rest/connections/connections.rb → models/user_filters/user_filters.rb} +1 -0
  65. data/lib/gooddata/models/user_filters/variable_user_filter.rb +14 -0
  66. data/lib/gooddata/rest/client.rb +32 -21
  67. data/lib/gooddata/rest/connection.rb +283 -11
  68. data/lib/gooddata/rest/connections/rest_client_connection.rb +47 -109
  69. data/lib/gooddata/version.rb +1 -1
  70. data/spec/data/column_based_permissions.csv +7 -0
  71. data/spec/data/column_based_permissions2.csv +6 -0
  72. data/spec/data/hello_world_process/hello_world.rb +3 -1
  73. data/spec/data/line_based_permissions.csv +3 -0
  74. data/spec/data/m_n_model/blueprint.json +76 -0
  75. data/spec/data/{model_view.json → wire_models/model_view.json} +0 -0
  76. data/spec/data/wire_models/nu_model.json +3046 -0
  77. data/spec/helpers/process_helper.rb +2 -2
  78. data/spec/helpers/project_helper.rb +29 -0
  79. data/spec/helpers/schedule_helper.rb +1 -1
  80. data/spec/integration/command_datawarehouse_spec.rb +32 -0
  81. data/spec/integration/create_project_spec.rb +0 -1
  82. data/spec/integration/full_process_schedule_spec.rb +13 -5
  83. data/spec/integration/full_project_spec.rb +2 -1
  84. data/spec/integration/over_to_user_filters_spec.rb +92 -0
  85. data/spec/integration/project_spec.rb +233 -0
  86. data/spec/integration/rest_spec.rb +209 -0
  87. data/spec/integration/user_filters_spec.rb +193 -0
  88. data/spec/integration/variables_spec.rb +196 -0
  89. data/spec/unit/commands/command_auth_spec.rb +0 -7
  90. data/spec/unit/commands/command_process_spec.rb +10 -13
  91. data/spec/unit/core/connection_spec.rb +0 -19
  92. data/spec/unit/helpers/global_helpers_spec.rb +57 -0
  93. data/spec/unit/models/domain_spec.rb +80 -40
  94. data/spec/unit/models/from_wire_spec.rb +8 -1
  95. data/spec/unit/models/params_spec.rb +6 -6
  96. data/spec/unit/models/profile_spec.rb +23 -22
  97. data/spec/unit/models/project_blueprint_spec.rb +1 -6
  98. data/spec/unit/models/project_spec.rb +331 -286
  99. data/spec/unit/models/schedule_spec.rb +39 -14
  100. data/spec/unit/models/user_filters_spec.rb +89 -0
  101. data/spec/unit/models/variable_spec.rb +259 -0
  102. metadata +31 -7
  103. data/lib/gooddata/rest/connections/dummy_connection.rb +0 -52
  104. data/spec/unit/core/rest_spec.rb +0 -106
@@ -5,6 +5,7 @@ require_relative 'nil_logger'
5
5
  module GoodData
6
6
  class << self
7
7
  attr_writer :logger
8
+ attr_writer :stats
8
9
 
9
10
  # Turn logging on
10
11
  #
@@ -43,5 +44,17 @@ module GoodData
43
44
  def logger
44
45
  @logger ||= NilLogger.new
45
46
  end
47
+
48
+ def stats_on
49
+ @stats = true
50
+ end
51
+
52
+ def stats_on? # rubocop:disable Style/TrivialAccessors
53
+ @stats
54
+ end
55
+
56
+ def stats_off
57
+ @stats = false
58
+ end
46
59
  end
47
60
  end
@@ -27,7 +27,7 @@ module GoodData
27
27
  #
28
28
  # ### Examples
29
29
  #
30
- # GoodData.post '/gdc/projects', { ... }
30
+ # client.post '/gdc/projects', { ... }
31
31
  #
32
32
  def post(path, data = {}, options = {})
33
33
  connection.post path, data, options
@@ -44,7 +44,7 @@ module GoodData
44
44
  #
45
45
  # ### Examples
46
46
  #
47
- # GoodData.put '/gdc/projects', { ... }
47
+ # client.put '/gdc/projects', { ... }
48
48
  #
49
49
  def put(path, data, options = {})
50
50
  connection.put path, data, options
@@ -64,8 +64,12 @@ module GoodData
64
64
  connection.delete path, options
65
65
  end
66
66
 
67
- def upload_to_user_webdav(file, options = {})
68
- u = URI(GoodData.project.links['uploads'])
67
+ # Upload to user directory
68
+ # @return [String]
69
+ def upload_to_user_webdav(file, options = { :project => GoodData.project })
70
+ options = merge_options(options)
71
+ project = options[:project]
72
+ u = URI(project.links['uploads'])
69
73
  url = URI.join(u.to_s.chomp(u.path.to_s), '/uploads/')
70
74
 
71
75
  connection.upload(file, options.merge(
@@ -73,28 +77,40 @@ module GoodData
73
77
  ))
74
78
  end
75
79
 
76
- def get_project_webdav_path(_file, _options = {})
77
- u = URI(GoodData.project.links['uploads'])
78
- URI.join(u.to_s.chomp(u.path.to_s), '/project-uploads/', "#{GoodData.project.pid}/")
80
+ # Get WebDav directory for project data
81
+ # @return [String]
82
+ def get_project_webdav_path(file, options = { :project => GoodData.project })
83
+ options = merge_options(options)
84
+ project = options[:project]
85
+ project.get_project_webdav_path(file)
79
86
  end
80
87
 
81
- def upload_to_project_webdav(file, options = {})
88
+ # Upload to project directory
89
+ def upload_to_project_webdav(file, options = { :project => GoodData.project })
90
+ options = merge_options(options)
82
91
  webdav_filename = File.basename(file)
83
92
  url = get_project_webdav_path(webdav_filename, options)
84
93
  connection.upload(file, options.merge(:staging_url => url))
85
94
  end
86
95
 
87
- def download_from_project_webdav(file, where, options = {})
96
+ # Download from project directory
97
+ def download_from_project_webdav(file, where, options = { :project => GoodData.project })
98
+ options = merge_options(options)
88
99
  url = get_project_webdav_path(file, options)
89
100
  connection.download(file, where, options.merge(:staging_url => url))
90
101
  end
91
102
 
92
- def get_user_webdav_path(_file, _options = {})
93
- u = URI(GoodData.project.links['uploads'])
94
- URI.join(u.to_s.chomp(u.path.to_s), '/uploads/')
103
+ # Get WebDav directory for user data
104
+ # @return [String]
105
+ def get_user_webdav_path(file, options = { :project => GoodData.project })
106
+ options = merge_options(options)
107
+ project = options[:project]
108
+ project.get_user_webdav_path(file)
95
109
  end
96
110
 
97
- def download_from_user_webdav(file, where, options = {})
111
+ # Download from user directory
112
+ def download_from_user_webdav(file, where, options = { :project => GoodData.project })
113
+ options = merge_options(options)
98
114
  url = get_user_webdav_path(file, options)
99
115
  connection.download(file, where, options.merge(:staging_url => url))
100
116
  end
@@ -114,7 +130,7 @@ module GoodData
114
130
  response = GoodData.get(link, :process => false)
115
131
  while response.code == code
116
132
  sleep sleep_interval
117
- GoodData::Rest::Client.retryable(:tries => 3, :on => RestClient::InternalServerError) do
133
+ GoodData::Rest::Client.retryable(:tries => 3, :refresh_token => proc { connection.refresh_token }) do
118
134
  sleep sleep_interval
119
135
  response = GoodData.get(link, :process => false)
120
136
  end
@@ -129,7 +145,7 @@ module GoodData
129
145
  # Generalizaton of poller. Since we have quite a variation of how async proceses are handled
130
146
  # this is a helper that should help you with resources where the information about "Are we done"
131
147
  # is inside the response. It expects the URI as an input where it can poll and a block that should
132
- # return either true -> 'meaning we are done' or false -> meaning sleep and repeat. It returns the
148
+ # return either false -> 'meaning we are done' or true -> meaning sleep and repeat. It returns the
133
149
  # value of last poll. In majority of cases these are the data that you need
134
150
  #
135
151
  # @param link [String] Link for polling
@@ -143,12 +159,20 @@ module GoodData
143
159
  response = get(link)
144
160
  while bl.call(response)
145
161
  sleep sleep_interval
146
- GoodData::Rest::Client.retryable(:tries => 3, :on => RestClient::InternalServerError) do
162
+ GoodData::Rest::Client.retryable(:tries => 3, :refresh_token => proc { client.connection.refresh_token }) do
147
163
  sleep sleep_interval
148
164
  response = get(link)
149
165
  end
150
166
  end
151
167
  response
152
168
  end
169
+
170
+ private
171
+
172
+ def merge_options(opts)
173
+ {
174
+ :project => GoodData.project
175
+ }.merge(opts)
176
+ end
153
177
  end
154
178
  end
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+
3
+ module GoodData
4
+ class UserInDifferentDomainError < RuntimeError
5
+ DEFAULT_MSG = 'User is already in different domain'
6
+
7
+ def initialize(msg = DEFAULT_MSG)
8
+ super(msg)
9
+ end
10
+ end
11
+ end
@@ -24,4 +24,12 @@ module Enumerable
24
24
  intermediate = pmap(&block)
25
25
  zip(intermediate).select { |x| x[1] }.map(&:first)
26
26
  end
27
+
28
+ def rjust(n, x)
29
+ Array.new([0, n - length].max, x) + self
30
+ end
31
+
32
+ def ljust(n, x)
33
+ dup.fill(x, length...n)
34
+ end
27
35
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module GoodData
4
4
  module SmallGoodZilla
5
+ PARSE_MAQL_OBJECT_REGEXP = /\[([^\]]+)\]/
6
+
5
7
  # Get IDs from MAQL string
6
8
  # @param a_maql_string Input MAQL string
7
9
  # @return [Array<String>] List of IDS
@@ -30,6 +32,28 @@ module GoodData
30
32
  a_maql_string.scan(/\?"([^\"]+)\"/).flatten
31
33
  end
32
34
 
35
+ # Pretty prints the MAQL expression. This basically means it finds out names of objects and elements and print their values instead of URIs
36
+ # @param expression [String] Expression to be beautified
37
+ # @return [String] Pretty printed MAQL expression
38
+ def self.pretty_print(expression, opts = { client: GoodData.connection, project: GoodData.project })
39
+ temp = expression.dup
40
+ pairs = expression.scan(PARSE_MAQL_OBJECT_REGEXP).pmap do |uri|
41
+ uri = uri.first
42
+ if uri =~ /elements/
43
+ [uri, Attribute.find_element_value(uri, opts)]
44
+ else
45
+ [uri, GoodData::MdObject[uri, opts].title]
46
+ end
47
+ end
48
+
49
+ pairs.each do |el|
50
+ uri = el[0]
51
+ obj = el[1]
52
+ temp.sub!(uri, obj)
53
+ end
54
+ temp
55
+ end
56
+
33
57
  def self.interpolate(values, dictionaries)
34
58
  {
35
59
  :facts => interpolate_values(values[:facts], dictionaries[:facts]),
@@ -6,16 +6,91 @@ require 'pathname'
6
6
  module GoodData
7
7
  module Helpers
8
8
  class << self
9
- def home_directory
10
- running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
11
- end
9
+ # A helper which allows you to diff two lists of objects. The objects
10
+ # can be arbitrary objects as long as they respond to to_hash because
11
+ # the diff is eventually done on hashes. It allows you to specify
12
+ # several options to allow you to limit on what the sameness test is done
13
+ #
14
+ # @param [Array<Object>] old_list List of objects that serves as a base for comparison
15
+ # @param [Array<Object>] new_list List of objects that is compared agianst the old_list
16
+ # @return [Hash] A structure that contains the result of the comparison. There are
17
+ # four keys.
18
+ # :added contains the list that are in new_list but were not in the old_list
19
+ # :added contains the list that are in old_list but were not in the new_list
20
+ # :same contains objects that are in both lists and they are the same
21
+ # :changed contains list of objects that changed along ith original, the new one
22
+ # and the list of changes
23
+ def diff(old_list, new_list, options = {})
24
+ old_list = old_list.map(&:to_hash)
25
+ new_list = new_list.map(&:to_hash)
12
26
 
13
- def running_on_windows?
14
- RUBY_PLATFORM =~ /mswin32|mingw32/
27
+ fields = options[:fields]
28
+ lookup_key = options[:key]
29
+
30
+ old_lookup = Hash[old_list.map { |v| [v[lookup_key], v] }]
31
+
32
+ res = {
33
+ :added => [],
34
+ :removed => [],
35
+ :changed => [],
36
+ :same => []
37
+ }
38
+
39
+ new_list.each do |new_obj|
40
+ old_obj = old_lookup[new_obj[lookup_key]]
41
+ if old_obj.nil?
42
+ res[:added] << new_obj
43
+ next
44
+ end
45
+
46
+ if fields
47
+ sliced_old_obj = old_obj.slice(*fields)
48
+ sliced_new_obj = new_obj.slice(*fields)
49
+ else
50
+ sliced_old_obj = old_obj
51
+ sliced_new_obj = new_obj
52
+ end
53
+ if sliced_old_obj != sliced_new_obj
54
+ difference = sliced_new_obj.to_a - sliced_old_obj.to_a
55
+ differences = Hash[*difference.mapcat { |x| x }]
56
+ res[:changed] << {
57
+ old_obj: old_obj,
58
+ new_obj: new_obj,
59
+ diff: differences
60
+ }
61
+ else
62
+ res[:same] << old_obj
63
+ end
64
+ end
65
+
66
+ new_lookup = Hash[new_list.map { |v| [v[lookup_key], v] }]
67
+ old_list.each do |old_obj|
68
+ new_obj = new_lookup[old_obj[lookup_key]]
69
+ if new_obj.nil?
70
+ res[:removed] << old_obj
71
+ next
72
+ end
73
+ end
74
+
75
+ res
15
76
  end
16
77
 
17
- def running_on_a_mac?
18
- RUBY_PLATFORM =~ /-darwin\d/
78
+ def create_lookup(collection, on)
79
+ lookup = {}
80
+ if on.is_a?(Array)
81
+ collection.each do |e|
82
+ key = e.values_at(*on)
83
+ lookup[key] = [] unless lookup.key?(key)
84
+ lookup[key] << e
85
+ end
86
+ else
87
+ collection.each do |e|
88
+ key = e[on]
89
+ lookup[key] = [] unless lookup.key?(key)
90
+ lookup[key] << e
91
+ end
92
+ end
93
+ lookup
19
94
  end
20
95
 
21
96
  ENCODED_PARAMS_KEY = :gd_encoded_params
@@ -94,6 +169,10 @@ module GoodData
94
169
  end
95
170
  end
96
171
 
172
+ def home_directory
173
+ running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
174
+ end
175
+
97
176
  def hash_dfs(thing, &block)
98
177
  if !thing.is_a?(Hash) && !thing.is_a?(Array) # rubocop:disable Style/GuardClause
99
178
  elsif thing.is_a?(Array)
@@ -109,14 +188,49 @@ module GoodData
109
188
  end
110
189
 
111
190
  # TODO: Implement without using ActiveSupport
112
- def sanitize_string(str, filter = /[^a-z_]/, replacement = '')
113
- str = ActiveSupport::Inflector.transliterate(str).downcase
114
- str.gsub(filter, replacement)
191
+ def humanize(str)
192
+ ActiveSupport::Inflector.humanize(str)
193
+ end
194
+
195
+ def join(master, slave, on, on2, options = {})
196
+ full_outer = options[:full_outer]
197
+
198
+ lookup = create_lookup(slave, on2)
199
+ marked_lookup = {}
200
+ results = master.reduce([]) do |a, line|
201
+ matching_values = lookup[line.values_at(*on)] || []
202
+ marked_lookup[line.values_at(*on)] = 1
203
+ if matching_values.empty?
204
+ a << line.to_hash
205
+ else
206
+ matching_values.each do |matching_value|
207
+ a << matching_value.to_hash.merge(line.to_hash)
208
+ end
209
+ end
210
+ a
211
+ end
212
+
213
+ if full_outer
214
+ (lookup.keys - marked_lookup.keys).each do |key|
215
+ puts lookup[key]
216
+ results << lookup[key].first.to_hash
217
+ end
218
+ end
219
+ results
220
+ end
221
+
222
+ def running_on_windows?
223
+ RUBY_PLATFORM =~ /mswin32|mingw32/
224
+ end
225
+
226
+ def running_on_a_mac?
227
+ RUBY_PLATFORM =~ /-darwin\d/
115
228
  end
116
229
 
117
230
  # TODO: Implement without using ActiveSupport
118
- def humanize(str)
119
- ActiveSupport::Inflector.humanize(str)
231
+ def sanitize_string(str, filter = /[^a-z_]/, replacement = '')
232
+ str = ActiveSupport::Inflector.transliterate(str).downcase
233
+ str.gsub(filter, replacement)
120
234
  end
121
235
 
122
236
  def underline(x)
@@ -3,13 +3,19 @@
3
3
  module GoodData
4
4
  module Mixin
5
5
  module Author
6
- # Gets Project Role Author
6
+ # Gets author of an object
7
7
  #
8
- # @return [GoodData::Profile] Project Role author
8
+ # @return [GoodData::Profile] object author
9
9
  def author
10
- url = meta['author']
11
- tmp = client.get url
12
- GoodData::Profile.new(tmp)
10
+ tmp = client.get(author_uri)
11
+ client.create(GoodData::Profile, tmp, project: project)
12
+ end
13
+
14
+ # Gets author URI of an object
15
+ #
16
+ # @return [String] object author URI
17
+ def author_uri
18
+ meta['author']
13
19
  end
14
20
  end
15
21
  end
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+
3
+ module GoodData
4
+ module Mixin
5
+ module IsDimension
6
+ # Returns true if the object is a dimension false otherwise
7
+ # @return [Boolean]
8
+ def dimension?
9
+ true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -37,7 +37,23 @@ module GoodData
37
37
  # new(GoodData.get uri) unless uri.nil?
38
38
  if uri # rubocop:disable Style/GuardClause
39
39
  raw = client.get(uri)
40
- client.create(self, raw, client: client, project: project)
40
+ md_class = self
41
+ case raw.keys.first
42
+ when 'attribute'
43
+ md_class = GoodData::Attribute
44
+ when 'metric'
45
+ md_class = GoodData::Metric
46
+ when 'projectDashboard'
47
+ md_class = GoodData::Dashboard
48
+ when 'report'
49
+ md_class = GoodData::Report
50
+ when 'reportDefinition'
51
+ md_class = GoodData::ReportDefinition
52
+ else
53
+ md_class = self
54
+ end
55
+
56
+ client.create(md_class, raw, client: client, project: project)
41
57
  end
42
58
  end
43
59
 
@@ -45,10 +45,18 @@ module GoodData
45
45
  if key.nil?
46
46
  result
47
47
  elsif key.respond_to?(:category)
48
- result.select { |item| item['category'] == key.category }
48
+ result = result.select { |item| item['category'] == key.category }
49
49
  else
50
- result.select { |item| item['category'] == key }
50
+ result = result.select { |item| item['category'] == key }
51
51
  end
52
+
53
+ if opts[:full]
54
+ result = result.map do |res|
55
+ GoodData::MdObject[res['link'], :client => c, :project => opts[:project]]
56
+ end
57
+ end
58
+
59
+ result
52
60
  end
53
61
 
54
62
  # Checks for dependency