gooddata 2.1.7-java → 2.1.12-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.gdc-ii-config.yaml +3 -0
  3. data/.rubocop.yml +7 -0
  4. data/.travis.yml +2 -4
  5. data/CHANGELOG.md +39 -0
  6. data/Dockerfile +17 -4
  7. data/Dockerfile.jruby +4 -4
  8. data/Dockerfile.ruby +5 -4
  9. data/SDK_VERSION +1 -1
  10. data/VERSION +1 -1
  11. data/bin/provision.sh +2 -0
  12. data/bin/release.sh +2 -0
  13. data/bin/rollout.sh +2 -0
  14. data/bin/run_brick.rb +31 -7
  15. data/bin/test_projects_cleanup.rb +10 -2
  16. data/bin/user_filters.sh +2 -0
  17. data/ci.rake +1 -1
  18. data/ci/bigquery/pom.xml +54 -0
  19. data/ci/redshift/pom.xml +73 -0
  20. data/ci/snowflake/pom.xml +57 -0
  21. data/dev-gooddata-sso.pub.encrypted +40 -40
  22. data/gdc_fossa_lcm.yaml +2 -0
  23. data/gdc_fossa_ruby_sdk.yaml +4 -0
  24. data/gooddata.gemspec +7 -3
  25. data/k8s/charts/lcm-bricks/Chart.yaml +1 -1
  26. data/k8s/charts/lcm-bricks/templates/prometheus/alertingRules.yaml +22 -12
  27. data/lcm.rake +14 -0
  28. data/lib/gooddata/bricks/middleware/execution_result_middleware.rb +68 -0
  29. data/lib/gooddata/bricks/middleware/logger_middleware.rb +2 -1
  30. data/lib/gooddata/bricks/middleware/mask_logger_decorator.rb +5 -1
  31. data/lib/gooddata/bricks/pipeline.rb +7 -0
  32. data/lib/gooddata/cloud_resources/bigquery/bigquery_client.rb +86 -0
  33. data/lib/gooddata/cloud_resources/bigquery/drivers/.gitkeepme +0 -0
  34. data/lib/gooddata/cloud_resources/cloud_resouce_factory.rb +28 -0
  35. data/lib/gooddata/cloud_resources/cloud_resource_client.rb +24 -0
  36. data/lib/gooddata/cloud_resources/cloud_resources.rb +12 -0
  37. data/lib/gooddata/cloud_resources/redshift/drivers/log4j.properties +15 -0
  38. data/lib/gooddata/cloud_resources/redshift/redshift_client.rb +101 -0
  39. data/lib/gooddata/cloud_resources/snowflake/drivers/.gitkeepme +0 -0
  40. data/lib/gooddata/cloud_resources/snowflake/snowflake_client.rb +84 -0
  41. data/lib/gooddata/exceptions/invalid_env_error.rb +15 -0
  42. data/lib/gooddata/helpers/data_helper.rb +10 -0
  43. data/lib/gooddata/helpers/global_helpers.rb +4 -0
  44. data/lib/gooddata/helpers/global_helpers_params.rb +4 -7
  45. data/lib/gooddata/lcm/actions/collect_clients.rb +6 -6
  46. data/lib/gooddata/lcm/actions/collect_dynamic_schedule_params.rb +6 -6
  47. data/lib/gooddata/lcm/actions/collect_segment_clients.rb +4 -1
  48. data/lib/gooddata/lcm/actions/collect_segments.rb +1 -2
  49. data/lib/gooddata/lcm/actions/collect_users_brick_users.rb +7 -6
  50. data/lib/gooddata/lcm/actions/create_segment_masters.rb +5 -3
  51. data/lib/gooddata/lcm/actions/set_master_project.rb +76 -0
  52. data/lib/gooddata/lcm/actions/synchronize_clients.rb +1 -1
  53. data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +1 -2
  54. data/lib/gooddata/lcm/actions/synchronize_ldm.rb +17 -2
  55. data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +23 -3
  56. data/lib/gooddata/lcm/actions/synchronize_users.rb +50 -30
  57. data/lib/gooddata/lcm/actions/update_release_table.rb +7 -1
  58. data/lib/gooddata/lcm/exceptions/lcm_execution_error.rb +16 -0
  59. data/lib/gooddata/lcm/helpers/release_table_helper.rb +16 -8
  60. data/lib/gooddata/lcm/lcm2.rb +27 -5
  61. data/lib/gooddata/models/domain.rb +17 -15
  62. data/lib/gooddata/models/execution.rb +0 -1
  63. data/lib/gooddata/models/execution_detail.rb +0 -1
  64. data/lib/gooddata/models/profile.rb +33 -11
  65. data/lib/gooddata/models/project.rb +45 -25
  66. data/lib/gooddata/models/project_creator.rb +2 -0
  67. data/lib/gooddata/models/schedule.rb +0 -1
  68. data/lib/gooddata/rest/client.rb +2 -2
  69. data/lib/gooddata/rest/connection.rb +5 -3
  70. data/rubydev_public.gpg.encrypted +51 -51
  71. data/rubydev_secret_keys.gpg.encrypted +109 -109
  72. metadata +38 -15
  73. data/lib/gooddata/extensions/hash.rb +0 -18
@@ -0,0 +1,16 @@
1
+ # Copyright (c) 2010-2019 GoodData Corporation. All rights reserved.
2
+ # This source code is licensed under the BSD-style license found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ module GoodData
6
+ class LcmExecutionError < RuntimeError
7
+ DEFAULT_MSG = 'Error during lcm execution'
8
+
9
+ attr_reader :summary_error
10
+
11
+ def initialize(summary_error, message = DEFAULT_MSG)
12
+ super(message)
13
+ @summary_error = summary_error
14
+ end
15
+ end
16
+ end
@@ -8,7 +8,7 @@ module GoodData
8
8
  module LCM2
9
9
  class Helpers
10
10
  DEFAULT_TABLE_NAME = 'LCM_RELEASE'
11
- DEFAULT_NFS_DIRECTORY = 'release-tables'
11
+ DEFAULT_NFS_DIRECTORY = '/release-tables'
12
12
 
13
13
  class << self
14
14
  def latest_master_project_from_ads(release_table_name, ads_client, segment_id)
@@ -25,22 +25,30 @@ module GoodData
25
25
  sorted.last
26
26
  end
27
27
 
28
- def latest_master_project_from_nfs(domain_id, segment_id)
29
- data = GoodData::Helpers::Csv.read_as_hash(path_to_release_table_file(domain_id, segment_id))
30
- data.sort_by { |master| master[:version] }
28
+ def latest_master_project_from_nfs(domain_id, data_product_id, segment_id)
29
+ file_path = path_to_release_table_file(domain_id, data_product_id, segment_id)
30
+ data = GoodData::Helpers::Csv.read_as_hash(file_path)
31
+ latest_master_project = data.sort_by { |master| master[:version] }
31
32
  .reverse.first
33
+
34
+ version_info = latest_master_project ? "master_pid=#{latest_master_project[:master_project_id]} version=#{latest_master_project[:version]}" : ""
35
+ GoodData.gd_logger.info "Getting latest master project: file=#{file_path} domain=#{domain_id} data_product=#{data_product_id} segment=#{segment_id} #{version_info}"
36
+ latest_master_project
32
37
  end
33
38
 
34
- def update_latest_master_to_nfs(domain_id, segment_id, master_pid, version)
39
+ def update_latest_master_to_nfs(domain_id, data_product_id, segment_id, master_pid, version)
40
+ file_path = path_to_release_table_file(domain_id, data_product_id, segment_id)
41
+ GoodData.gd_logger.info "Updating release table: file=#{file_path} domain=#{domain_id} data_product=#{data_product_id} segment=#{segment_id} master_pid=#{master_pid} version=#{version}" # rubocop:disable Metrics/LineLength
35
42
  GoodData::Helpers::Csv.ammend_line(
36
- path_to_release_table_file(domain_id, segment_id),
43
+ file_path,
37
44
  master_project_id: master_pid,
38
45
  version: version
39
46
  )
40
47
  end
41
48
 
42
- def path_to_release_table_file(domain_id, segment_id)
43
- [DEFAULT_NFS_DIRECTORY, domain_id, segment_id + '.csv'].join('/')
49
+ def path_to_release_table_file(domain_id, data_prod_id, segment_id)
50
+ nsf_directory = ENV['RELEASE_TABLE_NFS_DIRECTORY'] || DEFAULT_NFS_DIRECTORY
51
+ [nsf_directory, domain_id, data_prod_id + '-' + segment_id + '.csv'].join('/')
44
52
  end
45
53
  end
46
54
  end
@@ -18,6 +18,7 @@ require 'active_support/core_ext/hash/compact'
18
18
  require_relative 'actions/actions'
19
19
  require_relative 'dsl/dsl'
20
20
  require_relative 'helpers/helpers'
21
+ require_relative 'exceptions/lcm_execution_error'
21
22
 
22
23
  using TrueExtensions
23
24
  using FalseExtensions
@@ -105,6 +106,14 @@ module GoodData
105
106
  UpdateReleaseTable
106
107
  ],
107
108
 
109
+ release_set_master_project: [
110
+ EnsureReleaseTable,
111
+ CollectDataProduct,
112
+ SegmentsFilter,
113
+ SetMasterProject,
114
+ UpdateReleaseTable
115
+ ],
116
+
108
117
  provision: [
109
118
  EnsureReleaseTable,
110
119
  CollectDataProduct,
@@ -270,8 +279,14 @@ module GoodData
270
279
 
271
280
  GoodData.gd_logger.brick = mode
272
281
 
282
+ final_mode = if params.set_master_project && mode == 'release'
283
+ 'release_set_master_project'
284
+ else
285
+ mode
286
+ end
287
+
273
288
  # Get actions for mode specified
274
- actions = get_mode_actions(mode)
289
+ actions = get_mode_actions(final_mode)
275
290
 
276
291
  if params.actions
277
292
  actions = params.actions.map do |action|
@@ -304,6 +319,12 @@ module GoodData
304
319
  skip_actions.include?(action.name.split('::').last)
305
320
  end
306
321
 
322
+ sync_mode = params.fetch(:sync_mode, nil)
323
+ if mode == 'users' && %w[add_to_organization remove_from_organization].include?(sync_mode)
324
+ actions = actions.reject do |action|
325
+ %w[CollectDataProduct CollectSegments].include?(action.name.split('::').last)
326
+ end
327
+ end
307
328
  check_unused_params(actions, params)
308
329
  print_action_names(mode, actions)
309
330
 
@@ -357,10 +378,11 @@ module GoodData
357
378
 
358
379
  if errors.any?
359
380
  error_message = JSON.pretty_generate(errors)
360
- GoodData.logger.error(error_message)
361
-
362
- # Fail whole execution if there is any failed action
363
- fail(error_message) if strict_mode
381
+ if strict_mode
382
+ raise GoodData::LcmExecutionError.new(errors[0][:err], error_message)
383
+ else
384
+ GoodData.logger.error(error_message)
385
+ end
364
386
  end
365
387
 
366
388
  result
@@ -222,24 +222,26 @@ module GoodData
222
222
  domain = client.domain(domain)
223
223
  if id == :all
224
224
  GoodData.logger.warn("Retrieving all users from domain #{domain.name}")
225
- Enumerator.new do |y|
226
- page_limit = opts[:page_limit] || 1000
227
- offset = opts[:offset] || 0
228
- loop do
229
- begin
230
- tmp = client(opts).get("#{domain.uri}/users", params: { offset: offset, limit: page_limit })
231
- end
232
-
233
- tmp['accountSettings']['items'].each do |user_data|
234
- user = client.create(GoodData::Profile, user_data)
235
- y << user if user
236
- end
237
- break if tmp['accountSettings']['items'].count < page_limit
238
- offset += page_limit
225
+ all_users = []
226
+ page_limit = opts[:page_limit] || 1000
227
+ offset = opts[:offset] || 0
228
+ loop do
229
+ begin
230
+ tmp = client(opts).get("#{domain.uri}/users", params: { offset: offset, limit: page_limit })
239
231
  end
232
+
233
+ tmp['accountSettings']['items'].each do |user_data|
234
+ user = client.create(GoodData::Profile, user_data)
235
+ all_users << user if user
236
+ end
237
+ break if tmp['accountSettings']['items'].count < page_limit
238
+
239
+ offset += page_limit
240
240
  end
241
+
242
+ all_users
241
243
  else
242
- find_user_by_login(domain, id)
244
+ find_user_by_login(domain, id, opts)
243
245
  end
244
246
  end
245
247
 
@@ -5,7 +5,6 @@
5
5
  # LICENSE file in the root directory of this source tree.
6
6
 
7
7
  require_relative '../rest/resource'
8
- require_relative '../extensions/hash'
9
8
 
10
9
  module GoodData
11
10
  class Execution < Rest::Resource
@@ -5,7 +5,6 @@
5
5
  # LICENSE file in the root directory of this source tree.
6
6
 
7
7
  require_relative '../rest/resource'
8
- require_relative '../extensions/hash'
9
8
 
10
9
  module GoodData
11
10
  class ExecutionDetail < Rest::Resource
@@ -324,20 +324,42 @@ module GoodData
324
324
  end
325
325
 
326
326
  # Gets the array of projects
327
- #
327
+ # @param limit [Integer] maximum number of projects to get.
328
+ # @param offset [Integer] offset of the first project, start from 0.
328
329
  # @return [Array<GoodData::Project>] Array of project where account settings belongs to
329
- def projects(limit = nil)
330
+ def projects(limit = nil, offset = nil)
330
331
  url = @json['accountSetting']['links']['projects']
331
- query_params = ''
332
- if !limit.nil? && limit.is_a?(Integer) && limit > 0
333
- limit = [limit, 500].min
334
- query_params += "limit=#{limit}"
335
- end
336
- url += "?#{query_params}" unless query_params.empty?
337
- projects = client.get url
338
- projects['projects'].map do |project|
339
- client.create(GoodData::Project, project)
332
+
333
+ all_projects = []
334
+
335
+ raise ArgumentError, 'Params limit and offset are expected' if !offset.nil? && limit.nil?
336
+
337
+ if limit.nil?
338
+ url += "?limit=500"
339
+ loop do
340
+ projects = client.get url
341
+ projects['projects']['items'].each do |project|
342
+ all_projects << client.create(GoodData::Project, project)
343
+ end
344
+ if !projects['projects']['paging'].nil? && !projects['projects']['paging']['next'].nil?
345
+ url = projects['projects']['paging']['next']
346
+ else
347
+ break
348
+ end
349
+ end
350
+ else
351
+ limit = [limit, 500].min if limit.is_a?(Integer) && limit > 0
352
+
353
+ url += "?limit=#{limit}"
354
+ url += "&offset=#{offset}" if !offset.nil? && offset.is_a?(Integer) && offset > 0
355
+
356
+ projects = client.get url
357
+ projects['projects']['items'].each do |project|
358
+ all_projects << client.create(GoodData::Project, project)
359
+ end
340
360
  end
361
+
362
+ all_projects
341
363
  end
342
364
 
343
365
  # Saves object if dirty, clears dirty flag
@@ -67,9 +67,9 @@ module GoodData
67
67
  class << self
68
68
  # Returns an array of all projects accessible by
69
69
  # current user
70
- def all(opts = { client: GoodData.connection }, limit = nil)
70
+ def all(opts = { client: GoodData.connection }, limit = nil, offset = nil)
71
71
  c = GoodData.get_client(opts)
72
- c.user.projects(limit)
72
+ c.user.projects(limit, offset)
73
73
  end
74
74
 
75
75
  # Returns a Project object identified by given string
@@ -1546,26 +1546,28 @@ module GoodData
1546
1546
  # @return [Array<GoodData::User>] List of users
1547
1547
  def users(opts = {})
1548
1548
  client = client(opts)
1549
- Enumerator.new do |y|
1550
- offset = opts[:offset] || 0
1551
- limit = opts[:limit] || 1_000
1552
- loop do
1553
- tmp = client.get("/gdc/projects/#{pid}/users", params: { offset: offset, limit: limit })
1554
- tmp['users'].each do |user_data|
1555
- user = client.create(GoodData::Membership, user_data, project: self)
1556
-
1557
- if opts[:all]
1558
- y << user
1559
- elsif opts[:disabled]
1560
- y << user if user && user.disabled?
1561
- else
1562
- y << user if user && user.enabled?
1563
- end
1549
+ all_users = []
1550
+ offset = opts[:offset] || 0
1551
+ limit = opts[:limit] || 1_000
1552
+ loop do
1553
+ tmp = client.get("/gdc/projects/#{pid}/users", params: { offset: offset, limit: limit })
1554
+ tmp['users'].each do |user_data|
1555
+ user = client.create(GoodData::Membership, user_data, project: self)
1556
+
1557
+ if opts[:all]
1558
+ all_users << user
1559
+ elsif opts[:disabled]
1560
+ all_users << user if user&.disabled?
1561
+ else
1562
+ all_users << user if user&.enabled?
1564
1563
  end
1565
- break if tmp['users'].count < limit
1566
- offset += limit
1567
1564
  end
1565
+ break if tmp['users'].count < limit
1566
+
1567
+ offset += limit
1568
1568
  end
1569
+
1570
+ all_users
1569
1571
  end
1570
1572
 
1571
1573
  alias_method :members, :users
@@ -1604,14 +1606,19 @@ module GoodData
1604
1606
  def import_users(new_users, options = {})
1605
1607
  role_list = roles
1606
1608
  users_list = users
1607
- new_users = new_users.map { |x| ((x.is_a?(Hash) && x[:user] && x[:user].to_hash.merge(role: x[:role])) || x.to_hash).tap { |u| u[:login].downcase! } }
1608
1609
 
1609
1610
  GoodData.logger.warn("Importing users to project (#{pid})")
1611
+ new_users = new_users.map { |x| ((x.is_a?(Hash) && x[:user] && x[:user].to_hash.merge(role: x[:role])) || x.to_hash).tap { |u| u[:login].downcase! } }
1612
+ # First check that if groups are provided we have them set up
1613
+ user_groups_cache, change_groups = check_groups(new_users.map(&:to_hash).flat_map { |u| u[:user_group] || [] }.uniq, options[:user_groups_cache], options)
1610
1614
 
1611
- whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1615
+ unless change_groups.empty?
1616
+ new_users.each do |user|
1617
+ user[:user_group].map! { |e| change_groups[e].nil? ? e : change_groups[e] }
1618
+ end
1619
+ end
1612
1620
 
1613
- # First check that if groups are provided we have them set up
1614
- user_groups_cache = check_groups(new_users.map(&:to_hash).flat_map { |u| u[:user_group] || [] }.uniq, options[:user_groups_cache], options)
1621
+ whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1615
1622
 
1616
1623
  # conform the role on list of new users so we can diff them with the users coming from the project
1617
1624
  diffable_new_with_default_role = whitelisted_new_users.map do |u|
@@ -1758,7 +1765,20 @@ module GoodData
1758
1765
  def check_groups(specified_groups, user_groups_cache = nil, options = {})
1759
1766
  current_user_groups = user_groups if user_groups_cache.nil? || user_groups_cache.empty?
1760
1767
  groups = current_user_groups.map(&:name)
1761
- missing_groups = specified_groups - groups
1768
+ missing_groups = []
1769
+ change_groups = {}
1770
+ specified_groups.each do |group|
1771
+ found_group = groups.find { |name| name.casecmp(group).zero? }
1772
+ if found_group.nil?
1773
+ missing_groups << group
1774
+ else
1775
+ # Change groups when they have similar group name with difference of case sensitivity
1776
+ if found_group != group
1777
+ change_groups[group] = found_group
1778
+ GoodData.logger.warn("Group with name #{group} is existed in project with name #{found_group}.")
1779
+ end
1780
+ end
1781
+ end
1762
1782
  if options[:create_non_existing_user_groups]
1763
1783
  missing_groups.each do |g|
1764
1784
  GoodData.logger.info("Creating group #{g}")
@@ -1771,7 +1791,7 @@ module GoodData
1771
1791
  "#{groups.join(',')} and you asked for #{missing_groups.join(',')}"
1772
1792
  end
1773
1793
  end
1774
- current_user_groups
1794
+ [current_user_groups, change_groups]
1775
1795
  end
1776
1796
 
1777
1797
  # Update user
@@ -47,6 +47,8 @@ module GoodData
47
47
  unless response
48
48
  maql_diff_params = [:includeGrain]
49
49
  maql_diff_params << :excludeFactRule if opts[:exclude_fact_rule]
50
+ maql_diff_params << :includeDeprecated if opts[:include_deprecated]
51
+
50
52
  maql_diff_time = Benchmark.realtime do
51
53
  response = project.maql_diff(blueprint: bp, params: maql_diff_params)
52
54
  end
@@ -5,7 +5,6 @@
5
5
  # LICENSE file in the root directory of this source tree.
6
6
 
7
7
  require_relative '../rest/resource'
8
- require_relative '../extensions/hash'
9
8
  require_relative '../mixins/rest_resource'
10
9
  require_relative '../helpers/global_helpers'
11
10
 
@@ -188,11 +188,11 @@ module GoodData
188
188
  end
189
189
  end
190
190
 
191
- def projects(id = :all, limit = nil)
191
+ def projects(id = :all, limit = nil, offset = nil)
192
192
  if limit.nil?
193
193
  GoodData::Project[id, client: self]
194
194
  else
195
- GoodData::Project.all({ client: self }, limit)
195
+ GoodData::Project.all({ client: self }, limit, offset)
196
196
  end
197
197
  end
198
198
 
@@ -30,6 +30,7 @@ module GoodData
30
30
  LOGIN_PATH = '/gdc/account/login'
31
31
  TOKEN_PATH = '/gdc/account/token'
32
32
  KEYS_TO_SCRUB = [:password, :verifyPassword, :authorizationToken]
33
+ API_LEVEL = 2
33
34
 
34
35
  ID_LENGTH = 16
35
36
 
@@ -307,7 +308,6 @@ module GoodData
307
308
  # Remove when TT sent in headers. Currently we need to parse from body
308
309
  merge_headers!(:x_gdc_authtt => GoodData::Helpers.get_path(response, %w(userToken token)))
309
310
  rescue Exception => e # rubocop:disable RescueException
310
- GoodData.logger.error(e.message)
311
311
  raise e
312
312
  end
313
313
  end
@@ -350,6 +350,9 @@ module GoodData
350
350
  profile method.to_s.upcase, uri, request_id, stats_on do
351
351
  b = proc do
352
352
  params = fresh_request_params(request_id).merge(options)
353
+
354
+ params['X-GDC-VERSION'] = API_LEVEL
355
+
353
356
  case method
354
357
  when :get
355
358
  @server[uri].get(params, &user_block)
@@ -514,8 +517,7 @@ module GoodData
514
517
  begin
515
518
  request.execute
516
519
  rescue => e
517
- GoodData.logger.error("Error when uploading file #{filename}")
518
- raise e
520
+ raise "Error when uploading file #{filename}. Error: #{e}"
519
521
  end
520
522
  end
521
523
 
@@ -1,51 +1,51 @@
1
- eP6F85n9m2Dcr/lpjq3WaVmYq46J1yDgRut4w/mWReFkW2nPobJv224rqLY+
2
- JLHp6/mxJrrPlSR96TqIHvkQTyNIaZj9seFXLfmbdxg4uJOzlBP2rUEKB/DJ
3
- ywGjOEgpiec93Tkn1flHCmvsegXdU059EO/KCx/5jaBsOaNLP9lYfBnLjzmH
4
- E9vvpWp6V+qodHEpWDoUsXo5S2YuW5yN9Ht7WcRyXWHxhbQbPuclMWcnbuqu
5
- ieN08hNNRcqdoOYGnj4obLX+DKOiAId3f6mezJc1nhtfvf1ZNk1qXTO4D82V
6
- Rp3RJVB5wvyQoHuSn3+nycPITGxfb2xMwqwxn8FZMBhUjpFiBmHfnGRCu30u
7
- fQxKk/qyRnfYLfrqzgbD+i8k+xCqfuP5RGSKaRu0gzJGxLAJoIUCUWePvr5c
8
- UAOd9nijD1DnndpMYGsfKlvt6Ebv4yDewOjPDF5sNSGK6hxDDOvh2VE6ZzKc
9
- euLgWGR2XZvNVE6kjpq31+4mjBvvI3l+8YE9QoPEGA8XP33QtRjbGZFx3zgS
10
- XEYeKgSBaWCLeeWcSfHqXQ0sCCmcgiUr9nb9keLhllv1h4z79z69KKopc5iF
11
- YYEkhAczwI00vKNu5SBiYvDqOJu50P+/eJG6P4IGqq+S95TXIrxpOIlcIs22
12
- CqdCFtqsYwLRJJGI6X8OyTNinsiINmVsKynsZzb3WKJJuM53GxTe51oHvPWp
13
- 8YjhK2jAgH4xn1Uztgr/tRKel8ckosY9wZmvlOURiCxE112MPcjew1sZ7Bgx
14
- DvyRbSrgk4jPiaZ/iTbkqeksnmCILpxToTw/8m7owB48vqgmhM4qry9Kaq4x
15
- qsSdB3bLj/D80VF5+Gnhr0AjCnZts1DPsjELVrTrlMsOc168+rUI8UJLD8Ci
16
- hFECXlDD7QXjh3XJoD6PD5xP/sJg2iYeve7SC7qCddku1ouzOfDLKmi2Hrra
17
- nGdD/+1ggkjCTbessARa49LfTbvxIQWva+sySfcm6/xvS1iREQZwAPBQKqEL
18
- wgGqwTJhkdDlrBzEyx2tMXt3ZcfiQUlp/F1bp4ROb2bh5rFAcPbcdPfd3Enp
19
- spX8K0KP3HjoSnTgILhLAw8wbKuepYBcwQPX+7s3fyoPad+zE/4uq51NrFAF
20
- 2u1QLivw/8pAElx935RcGcCxI7BeHW2OEpTiP1oIllFjlxW50fR95N3AZ3eb
21
- H4gXR8e7gkW2ZaBfOzEIdHE08yTB8iUHsVQkWznH0ZUIAsv2hRCxfSs2/qKB
22
- kQglwdngAtvAxJzIdb7jgpZ/s1Tn4WbqtManVrnXwFteiWnOVSltPUcoKhPl
23
- 6U+CncomSzpnktlb0xBP6XxFX06EkDrAeky14NiT6FOjoFQOedYKvNLkmDBc
24
- SQt7ck8XHDRggkDJrWfn50ZF0p6RZC6FGU+PsFQszM96b1QAzx3lx8A+tmOj
25
- R1NOmWw28QKPlItGe+Cu9UTSHoHng4yKwEzgH10KiX8q9rosJT3V7DVQvWOx
26
- s3XBg2U/YeXXsmvfOX5k5kLRvsH2KKuEnTFx2lRl9bNgs0vWSkJha60wTcjQ
27
- GuVZ/y+rVMQM/tIjnI+IfZ35knz2vnRTLk0u6R2kJenTXahlwhvjwmol9y6H
28
- eixTfA4sdOUno8pvu7Ur8NVNpUsb/iZ/acf3nEOauqUCrixleZPcqc2vVtWk
29
- MxGgqSmaihZi8ZHD1Pryf3DwYB9ssSuV3BKthsHnsjOcVWksn2vdiXtogDwt
30
- kxnP/TLjbTZk3oAOIjyYzvEo9kVjb51qZO17a9rklo7CE89EBJZEyTdWpfhX
31
- cYczntRjhaFjBqFfJ3JhpyMvDfc2NN9C7dZy2f5BPuNI7QoJLgFxaz6qj8SW
32
- PhJcxmqLuqyqTS2VNAA5IdOASnt9LycV5xkGoUzSGc0efIsOSCKeVu2NqVqg
33
- HIBk3QV7Fk0/bv7U5ZN9nMPWgui/+2lbYTgqAC+wK6q/BRhZYY5jZBGbtPe/
34
- ukQUkh9w7WLgdJ9MblqyxHWa6T1ZPh9a1Agz2XCv6SQHj6qlk9KX3LvOba9F
35
- l7Za2d7Jv5ozLcpkhPW1ZAWAbtPG4T/Y7XzhSf+2SjZEIoKJv3VcL0lC+IE/
36
- ZUhGE6lNQLuesPGu3vKX6VuoeVj3PyMaWtzMIqrxVzrTCUlynRrPY2OM+SnS
37
- ftcthAOGApQ5Z9wUhMUDXDkPYMCny2joAK32XgFqZjq75Zljux4QgggEKTMY
38
- YXsfBHVNQ48zciP55IIMJ1oeC0gmLx1AqtA8R9j8frTnmwR1cTQtk4ydSDVt
39
- QKunn0dl35ZUG9XNS+LLnxV4aOjSqB9Hv8fgcIQFWSIz4HmLIienlVoPTsvY
40
- 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
- 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
- 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
- 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
- YjiKp34DzTRJ4dk1nYbgr8IG2eD6AXQHr+TzGzERMV2YPZlr9wZ2lNjcY7l0
45
- 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
- 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
- EkCr7vOnYL4bRKFzy6jru+906rxJwtZQP7fajTvjBmTJ5QLY8CNrpUMU8iOc
48
- 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
- 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
- 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
- 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