gooddata 2.1.8-java → 2.1.13-java

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -0
  3. data/.travis.yml +2 -4
  4. data/CHANGELOG.md +43 -0
  5. data/Dockerfile +19 -4
  6. data/Dockerfile.jruby +4 -4
  7. data/Dockerfile.ruby +5 -4
  8. data/README.md +2 -0
  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 +6 -2
  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/data_source_helpers.rb +47 -0
  44. data/lib/gooddata/helpers/global_helpers.rb +4 -0
  45. data/lib/gooddata/helpers/global_helpers_params.rb +6 -9
  46. data/lib/gooddata/lcm/actions/collect_clients.rb +6 -6
  47. data/lib/gooddata/lcm/actions/collect_dynamic_schedule_params.rb +6 -6
  48. data/lib/gooddata/lcm/actions/collect_segment_clients.rb +4 -1
  49. data/lib/gooddata/lcm/actions/collect_segments.rb +1 -2
  50. data/lib/gooddata/lcm/actions/collect_users_brick_users.rb +7 -6
  51. data/lib/gooddata/lcm/actions/create_segment_masters.rb +5 -3
  52. data/lib/gooddata/lcm/actions/migrate_gdc_date_dimension.rb +116 -0
  53. data/lib/gooddata/lcm/actions/set_master_project.rb +76 -0
  54. data/lib/gooddata/lcm/actions/synchronize_clients.rb +1 -1
  55. data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +1 -2
  56. data/lib/gooddata/lcm/actions/synchronize_ldm.rb +20 -3
  57. data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +23 -3
  58. data/lib/gooddata/lcm/actions/synchronize_users.rb +50 -30
  59. data/lib/gooddata/lcm/actions/update_release_table.rb +7 -1
  60. data/lib/gooddata/lcm/exceptions/lcm_execution_error.rb +16 -0
  61. data/lib/gooddata/lcm/helpers/release_table_helper.rb +16 -8
  62. data/lib/gooddata/lcm/lcm2.rb +28 -5
  63. data/lib/gooddata/models/domain.rb +17 -15
  64. data/lib/gooddata/models/execution.rb +0 -1
  65. data/lib/gooddata/models/execution_detail.rb +0 -1
  66. data/lib/gooddata/models/from_wire.rb +1 -0
  67. data/lib/gooddata/models/process.rb +11 -3
  68. data/lib/gooddata/models/profile.rb +33 -11
  69. data/lib/gooddata/models/project.rb +120 -31
  70. data/lib/gooddata/models/project_creator.rb +2 -0
  71. data/lib/gooddata/models/schedule.rb +0 -1
  72. data/lib/gooddata/rest/client.rb +2 -2
  73. data/lib/gooddata/rest/connection.rb +5 -3
  74. data/rubydev_public.gpg.encrypted +51 -51
  75. data/rubydev_secret_keys.gpg.encrypted +109 -109
  76. metadata +32 -13
  77. data/lib/gooddata/extensions/hash.rb +0 -18
@@ -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
@@ -261,21 +261,26 @@ module GoodData
261
261
  # @option ads_output_stage_uri Uri of the source output stage. It must be in the same domain as the target project.
262
262
  def transfer_processes(from_project, to_project, options = {})
263
263
  options = GoodData::Helpers.symbolize_keys(options)
264
+ aliases = {}
264
265
  to_project_processes = to_project.processes
265
266
  additional_hidden_params = options[:additional_hidden_params] || {}
266
267
  result = from_project.processes.uniq(&:name).map do |process|
267
- fail "The process name #{process.name} must be unique in transfered project #{to_project}" if to_project_processes.count { |p| p.name == process.name } > 1
268
+ fail "The process name #{process.name} must be unique in transferred project #{to_project}" if to_project_processes.count { |p| p.name == process.name } > 1
268
269
  next if process.type == :dataload || process.add_v2_component?
270
+ collect_process_aliases(process.data, from_project.client, aliases)
269
271
 
270
272
  to_process = to_project_processes.find { |p| p.name == process.name }
271
273
 
274
+ data_sources = GoodData::Helpers.symbolize_keys_recursively!(process.data_sources)
275
+ data_sources = replace_data_source_ids(data_sources, to_project.client, aliases)
272
276
  to_process = if process.path
273
277
  to_process.delete if to_process
274
- GoodData::Process.deploy_from_appstore(process.path, name: process.name, client: to_project.client, project: to_project)
278
+ Process.deploy_from_appstore(process.path, name: process.name, client: to_project.client, project: to_project, data_sources: data_sources)
275
279
  elsif process.component
276
280
  to_process.delete if to_process
277
281
  process_hash = GoodData::Helpers::DeepMergeableHash[GoodData::Helpers.symbolize_keys(process.to_hash)].deep_merge(additional_hidden_params)
278
- GoodData::Process.deploy_component(process_hash, project: to_project, client: to_project.client)
282
+ process_hash = replace_process_data_source_ids(process_hash, to_project.client, aliases)
283
+ Process.deploy_component(process_hash, project: to_project, client: to_project.client)
279
284
  else
280
285
  Dir.mktmpdir('etl_transfer') do |dir|
281
286
  dir = Pathname(dir)
@@ -283,11 +288,10 @@ module GoodData
283
288
  File.open(filename, 'w') do |f|
284
289
  f << process.download
285
290
  end
286
-
287
291
  if to_process
288
- to_process.deploy(filename, type: process.type, name: process.name)
292
+ to_process.deploy(filename, type: process.type, name: process.name, data_sources: data_sources)
289
293
  else
290
- to_project.deploy_process(filename, type: process.type, name: process.name)
294
+ to_project.deploy_process(filename, type: process.type, name: process.name, data_sources: data_sources)
291
295
  end
292
296
  end
293
297
  end
@@ -318,6 +322,56 @@ module GoodData
318
322
  result.compact
319
323
  end
320
324
 
325
+ def collect_process_aliases(process_data, client, aliases)
326
+ data_sources = process_data.dig('process', 'dataSources')
327
+ unless data_sources.blank?
328
+ data_sources.map do |data_source|
329
+ get_data_source_alias(data_source['id'], client, aliases)
330
+ end
331
+ end
332
+ component = process_data.dig('process', 'component')
333
+ get_data_source_alias(component['configLocation']['dataSourceConfig']['id'], client, aliases) if component&.dig('configLocation', 'dataSourceConfig')
334
+ aliases
335
+ end
336
+
337
+ def get_data_source_alias(data_source_id, client, aliases)
338
+ unless aliases[data_source_id]
339
+ data_source = GoodData::Helpers.get_data_source_by_id(data_source_id, client)
340
+ if data_source&.dig('dataSource', 'alias')
341
+ aliases[data_source_id] = {
342
+ :type => get_data_source_type(data_source),
343
+ :alias => data_source['dataSource']['alias']
344
+ }
345
+ end
346
+ end
347
+ aliases[data_source_id]
348
+ end
349
+
350
+ def get_data_source_type(data_source_data)
351
+ data_source_data&.dig('dataSource', 'connectionInfo') ? data_source_data['dataSource']['connectionInfo'].first[0].upcase : ""
352
+ end
353
+
354
+ def replace_process_data_source_ids(process_data, client, aliases)
355
+ component = process_data.dig(:process, :component)
356
+ if component&.dig(:configLocation, :dataSourceConfig)
357
+ the_alias = aliases[component[:configLocation][:dataSourceConfig][:id]]
358
+ process_data[:process][:component][:configLocation][:dataSourceConfig][:id] = GoodData::Helpers.verify_data_source_alias(the_alias, client)
359
+ end
360
+ process_data[:process][:dataSources] = replace_data_source_ids(process_data[:process][:dataSources], client, aliases)
361
+ process_data
362
+ end
363
+
364
+ def replace_data_source_ids(data_sources, client, aliases)
365
+ array_data_sources = []
366
+ if data_sources && !data_sources.empty?
367
+ data_sources.map do |data_source|
368
+ new_id = GoodData::Helpers.verify_data_source_alias(aliases[data_source[:id]], client)
369
+ array_data_sources.push(:id => new_id)
370
+ end
371
+ end
372
+ array_data_sources
373
+ end
374
+
321
375
  def transfer_user_groups(from_project, to_project)
322
376
  from_project.user_groups.map do |ug|
323
377
  # migrate groups
@@ -625,6 +679,7 @@ module GoodData
625
679
  def blueprint(options = {})
626
680
  options = { include_ca: true }.merge(options)
627
681
  result = client.get("/gdc/projects/#{pid}/model/view", params: { includeDeprecated: true, includeGrain: true, includeCA: options[:include_ca] })
682
+
628
683
  polling_url = result['asyncTask']['link']['poll']
629
684
  model = client.poll_on_code(polling_url, options)
630
685
  bp = GoodData::Model::FromWire.from_wire(model, options)
@@ -1546,26 +1601,28 @@ module GoodData
1546
1601
  # @return [Array<GoodData::User>] List of users
1547
1602
  def users(opts = {})
1548
1603
  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
1604
+ all_users = []
1605
+ offset = opts[:offset] || 0
1606
+ limit = opts[:limit] || 1_000
1607
+ loop do
1608
+ tmp = client.get("/gdc/projects/#{pid}/users", params: { offset: offset, limit: limit })
1609
+ tmp['users'].each do |user_data|
1610
+ user = client.create(GoodData::Membership, user_data, project: self)
1611
+
1612
+ if opts[:all]
1613
+ all_users << user
1614
+ elsif opts[:disabled]
1615
+ all_users << user if user&.disabled?
1616
+ else
1617
+ all_users << user if user&.enabled?
1564
1618
  end
1565
- break if tmp['users'].count < limit
1566
- offset += limit
1567
1619
  end
1620
+ break if tmp['users'].count < limit
1621
+
1622
+ offset += limit
1568
1623
  end
1624
+
1625
+ all_users
1569
1626
  end
1570
1627
 
1571
1628
  alias_method :members, :users
@@ -1604,14 +1661,19 @@ module GoodData
1604
1661
  def import_users(new_users, options = {})
1605
1662
  role_list = roles
1606
1663
  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
1664
 
1609
1665
  GoodData.logger.warn("Importing users to project (#{pid})")
1666
+ 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! } }
1667
+ # First check that if groups are provided we have them set up
1668
+ 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
1669
 
1611
- whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1670
+ unless change_groups.empty?
1671
+ new_users.each do |user|
1672
+ user[:user_group].map! { |e| change_groups[e].nil? ? e : change_groups[e] }
1673
+ end
1674
+ end
1612
1675
 
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)
1676
+ whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1615
1677
 
1616
1678
  # conform the role on list of new users so we can diff them with the users coming from the project
1617
1679
  diffable_new_with_default_role = whitelisted_new_users.map do |u|
@@ -1758,7 +1820,20 @@ module GoodData
1758
1820
  def check_groups(specified_groups, user_groups_cache = nil, options = {})
1759
1821
  current_user_groups = user_groups if user_groups_cache.nil? || user_groups_cache.empty?
1760
1822
  groups = current_user_groups.map(&:name)
1761
- missing_groups = specified_groups - groups
1823
+ missing_groups = []
1824
+ change_groups = {}
1825
+ specified_groups.each do |group|
1826
+ found_group = groups.find { |name| name.casecmp(group).zero? }
1827
+ if found_group.nil?
1828
+ missing_groups << group
1829
+ else
1830
+ # Change groups when they have similar group name with difference of case sensitivity
1831
+ if found_group != group
1832
+ change_groups[group] = found_group
1833
+ GoodData.logger.warn("Group with name #{group} is existed in project with name #{found_group}.")
1834
+ end
1835
+ end
1836
+ end
1762
1837
  if options[:create_non_existing_user_groups]
1763
1838
  missing_groups.each do |g|
1764
1839
  GoodData.logger.info("Creating group #{g}")
@@ -1771,7 +1846,7 @@ module GoodData
1771
1846
  "#{groups.join(',')} and you asked for #{missing_groups.join(',')}"
1772
1847
  end
1773
1848
  end
1774
- current_user_groups
1849
+ [current_user_groups, change_groups]
1775
1850
  end
1776
1851
 
1777
1852
  # Update user
@@ -1902,6 +1977,20 @@ module GoodData
1902
1977
  [user, roles]
1903
1978
  end
1904
1979
 
1980
+ def upgrade_custom_v2(message, options = {})
1981
+ uri = "/gdc/md/#{pid}/datedimension/upgrade"
1982
+ poll_result = client&.post(uri, message)
1983
+
1984
+ return poll_result['wTaskStatus']['status'] if poll_result['wTaskStatus'] && poll_result['wTaskStatus']['status']
1985
+
1986
+ polling_uri = poll_result['asyncTask']['link']['poll']
1987
+ result = client&.poll_on_response(polling_uri, options) do |body|
1988
+ body && body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
1989
+ end
1990
+
1991
+ result['wTaskStatus']['status'] == 'OK' ? 'OK' : 'FAIL'
1992
+ end
1993
+
1905
1994
  def add
1906
1995
  @add ||= GoodData::AutomatedDataDistribution.new(self)
1907
1996
  @add
@@ -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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 27g/nTIyUrFzy0d4N6GNFpT54qhyiMjgm9DUXyxaBkESjP0RaGCVAOqQczsA
51
- HIX3ye38dSToa7d4ia2qgEDJUccN8EwAEMqtaxwIRe8/e152Ems=
1
+ rpX3+WxQzYHZ7UHoJVfxML2DACQadPwTVHMV1Saf9n2ESLHSq324l424EJM0
2
+ NkWh+M+oKVIdQiNR+arCaMiQMlxMGd0v00f8+A1jpI7tWy/tnCO7Nicmqoy8
3
+ Ip2A/+xt0Cv46qTTZG6n3p9rdobM9Vmft7KiJae+01rS8fWY5cn4vURFb/3i
4
+ 6juEE9MaV9KzReoBnA0KyhrEaG9/B7JzjQkpIBd56uBJfVAI3kKQsUlf3yKc
5
+ 09hQW6tXBvW1ygeYSh2U3TIJ00ZJkWhx9mUyh3H0HlFT0w7Zr21L05dDJFK1
6
+ /0BJmRmQl18otPuJ2hXOO1a6238C8GpMe2Thf/F6DnzlRrM5EQkYoZILCTdy
7
+ pVJC7ecS9LhO1I8L3cxSIBUW0UM8paoXy4XCvxeLtClGhU3gHCfHR9BE19D8
8
+ 7zfveOYtFj/sRqudfHNHgrg9yRSRp0TDMfY4iBZZHHEIkYwbBlRkBMEplpG4
9
+ x34ROjvgTRrHCTgGnzgof3OU/3dAIUohy6LYUhbFauxVLoekhEM3GsmEPP0D
10
+ VOPcelh+ROyLGGtH5xkh7duLaGaRNL2uAFAS7q3aDFFBeTu2FPa3cLbe88dc
11
+ SkgmVMHW6kiAgkCUJ/Qv0EXmwe5GAvFfaMybQIEHQECKA8+kD9mhtBflbjkP
12
+ hfEsRSEIk2pyyyg+MhVQ94xNXREq9/YQSP3seWp/47uAbQBKZATCGozSKuX5
13
+ pzm+uQAaivCFVfuRnfJHGdAFSU8Q38p4Uod1SYhPvxbC1/Hrg1aigDR0Zmft
14
+ CvWpM9wBkYIqKC4GyElIwhqwEiqgpjJGqRjZAOB1FW/sJtEvYUyCAxzacjJn
15
+ C8PTvIyL6cFtcyvrc97qTV4lcrm1WWz/yfTfmwe1Yq61852DGtoI4/M8iIA2
16
+ S6F2s2BhzZIHZcACN2pyPaF4bU4SWBvB565XrdxFpyNiCXr81Dk3q6WIDTSF
17
+ pUNyvrrDmxcsIE3APM5CBNlzxrWiaK9nfOeKWwdV4u6dR68n10SY3wQ1dxcP
18
+ UEN3jMQz7S7+atH2nROq7nIyiZ+lIjcgEl2XMKwkbApiBHIErZnPCpAVwG7b
19
+ Lmg4OxYFaEblQhowDULjlyxxT5W3IbmzOC0UdQEgldF+PSOHhB5GoIJZKJex
20
+ jdmrCZNJY8V0zdpAWmStReoRGghSiidk3wUPbRu1aBq9o/akBdvvY5OlUkME
21
+ Lg4zd85Jilb5ZuMBqhQPtAmwgwh469ye/IxwrBj7bQNoZA1mU07oEEVqvLzP
22
+ S6MLeAAufJdqSDcBLZvkcYYy51bD2kqNbZuqonQeuyetNR+k8bCPjU+AdX2i
23
+ 0tvIC9/0PgeY3NazXuGCr1NMFS14F/dPjqmJSccpVdApqnaHiiTWKEIix9I4
24
+ DnvhVoXsYNXp4puhm5WHnJwIsa4Hy5mc5+NnXkWjZBUyFI9R76wMxKx2xcpa
25
+ foQbhplaZc3qb/lHhhppVHx6VG6bJX4eePm9sBNHB6brEZjUKJ/Y7wrDTeVo
26
+ qDEZ1Gfu1TxaQIyyHAOGdqhh3/LieUn8AhDYfSm4IWw4QqTU6VlumRWNemWd
27
+ ThGBgynWNuVy6w4pTyku+RSztrV3JCTmWtNJwsmyGIE3fISC/xMQOtLIiBrJ
28
+ n9Lhofa/coVkK2Ff5KoIPaU7kZ+67whLI0kD/2gV+NA1y0tiM4kEqw2IDaOX
29
+ TfAWn3vlnMmBDPsRm2y+//dZtPwAb1Q77/kR5wv691cxar3rdY6+Hu7SbSJF
30
+ 0Xee3s7cgHHSAOV086vroMvfJVjIeD8JDBrK0hgc/tGCogJyKhyMcoyrlnB5
31
+ t9jrY14a/Ya8HEBGHAOchuMbxn095dMezmRCD9TQkv+Zao+w/nCRPZukomsP
32
+ FUrIw2BIxB8SXiOA201cHfjztxwBIQ/HJlRbd8/HcvCjKxYS+dqCJ3e1sNuP
33
+ +ZzF4fheucsXEKQowPPCcGw3A2hn1Rf2WZRTB+LR7bYeBvlX5v7WyqYzuRBw
34
+ CGOW9KYe7rqGozXWC8SmzKf1zY+0Rc5Ftj7UDW4kz+Q5L8WTCa8QG8qa8a8X
35
+ vVR8qu7KSuuL1s219W2KeLYa1P4JOWmFzEaZ0xAesNW+9LEI7G9QzeFyUeWt
36
+ ARJvaTyxG29K8PVRgsbECgQSzzEOagpNP2r1bz5QmV2vLbIoMvR8jouKpcjJ
37
+ dlmWz6rB6Zxp2AuTDpbnNudXAZI9d7jjnoOQ6dkGSJyx7CTyIEWrKCbhAoFY
38
+ +JJ+HoVbV3J8Jd7qjdP4W5sm8uU2th/3CzOiXltTYta7tH8xFthvphv/q2ev
39
+ bi1yTPovLT0HUYb5sRxuk/QV5YuoGy04bHoT5+beohnM5UjsKVIZsJ4XYqNM
40
+ xoI6+32r2NUXcAgINGeI1LVKOc6/7T/Tg/Q+KjlIcQD2+2BKW4M51+Eep1W3
41
+ afeDOKRK8BjBmgzElGiOiLcc+RrvW6f61qLPyWC2nJ9THdwmYFaMurGrM6ed
42
+ j+ZkUTdCFOnD88sbtUQ1kAKs44vggWmYitUqdyI6ZmReGj9kmbAu3NHZ7c6g
43
+ VvPpJkoVMdJb8h0up4Zrdk46WbdEg/+LixwfAs/oLVwEwa6bJp+6xDS2LcOR
44
+ GPMLLmTtD3iddFDfeU8j/mhGCsO0AzacmxbsJDYr++iGAkTBvx6sP3HwjhPe
45
+ h3zvckeGZOg+feJhFTmol0wUVKN9JYXlreb7qsMfaawbQhwscc7WBVDnh5DV
46
+ pnx62Eqe+NjshYyOGn56NW/u9nDyd6jrP+JTuQvx9QB5wU/Uc71Ac+d94v8I
47
+ pUo03roZPsmXBN/8XGpoZOOIfkTNOI0B/1dpuYaUacupdBmZxxIrLmdw6CO7
48
+ U4P8MBCQUvHnaU/RE+uChgOJxg7rM2j76FcvBYpU7U80x83T3NNxog/GGRrH
49
+ TVAE9pw+CcFtOVtkyZz4ORy5KU7nbOIVOVZOF18BC9pJq5/qEwgAYIEZe/Go
50
+ CdB6Lad/+w1ymY8S+pDPYJZ3xc10ijQjNo8ahJe9H/ZNgxBq7nS+6jvGFN1l
51
+ Wt4ZJD/zQtokqjEoGH/xNks3FzboCwHf05iZ69+RQFLljaPqZ/Q=