gooddata 2.1.11 → 2.1.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gdc-ii-config.yaml +1 -1
  3. data/.sonar.settings +4 -0
  4. data/.travis.yml +4 -3
  5. data/CHANGELOG.md +34 -0
  6. data/Dockerfile +9 -3
  7. data/LICENSE +30 -22
  8. data/LICENSE.rb +1 -1
  9. data/README.md +19 -1
  10. data/Rakefile +8 -1
  11. data/SDK_VERSION +1 -1
  12. data/VERSION +1 -1
  13. data/bin/run_brick.rb +3 -0
  14. data/bin/test_projects_cleanup.rb +7 -3
  15. data/ci/postgresql/pom.xml +57 -0
  16. data/dev-gooddata-sso.pub.encrypted +40 -40
  17. data/gdc_fossa_lcm.yaml +2 -0
  18. data/gdc_fossa_ruby_sdk.yaml +5 -0
  19. data/gooddata.gemspec +4 -5
  20. data/k8s/charts/lcm-bricks/Chart.yaml +1 -1
  21. data/k8s/charts/lcm-bricks/templates/prometheus/alertingRules.yaml +32 -12
  22. data/lib/gooddata.rb +2 -0
  23. data/lib/gooddata/cloud_resources/{cloud_resouce_factory.rb → cloud_resource_factory.rb} +8 -0
  24. data/lib/gooddata/cloud_resources/cloud_resources.rb +1 -1
  25. data/lib/gooddata/cloud_resources/postgresql/drivers/.gitkeepme +0 -0
  26. data/lib/gooddata/cloud_resources/postgresql/postgresql_client.rb +107 -0
  27. data/lib/gooddata/commands/scaffold.rb +9 -10
  28. data/lib/gooddata/core/nil_logger.rb +3 -1
  29. data/lib/gooddata/helpers/data_helper.rb +1 -2
  30. data/lib/gooddata/helpers/global_helpers.rb +6 -5
  31. data/lib/gooddata/helpers/global_helpers_params.rb +2 -2
  32. data/lib/gooddata/lcm/actions/base_action.rb +0 -2
  33. data/lib/gooddata/lcm/actions/collect_tagged_objects.rb +2 -1
  34. data/lib/gooddata/lcm/actions/migrate_gdc_date_dimension.rb +116 -0
  35. data/lib/gooddata/lcm/actions/synchronize_ldm.rb +10 -1
  36. data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +4 -0
  37. data/lib/gooddata/lcm/actions/synchronize_users.rb +7 -6
  38. data/lib/gooddata/lcm/lcm2.rb +1 -2
  39. data/lib/gooddata/lcm/types/base_type.rb +0 -2
  40. data/lib/gooddata/mixins/md_object_query.rb +8 -6
  41. data/lib/gooddata/models/blueprint/project_blueprint.rb +0 -2
  42. data/lib/gooddata/models/client.rb +14 -12
  43. data/lib/gooddata/models/data_source.rb +664 -0
  44. data/lib/gooddata/models/domain.rb +1 -2
  45. data/lib/gooddata/models/from_wire.rb +1 -0
  46. data/lib/gooddata/models/metadata/scheduled_mail.rb +1 -1
  47. data/lib/gooddata/models/process.rb +11 -3
  48. data/lib/gooddata/models/project.rb +128 -19
  49. data/lib/gooddata/models/user_filters/user_filter_builder.rb +0 -1
  50. data/lib/gooddata/models/user_group.rb +0 -1
  51. data/lib/gooddata/rest/connection.rb +6 -4
  52. data/rubydev_public.gpg.encrypted +51 -51
  53. data/rubydev_secret_keys.gpg.encrypted +109 -109
  54. metadata +17 -25
  55. data/DEPENDENCIES.md +0 -880
@@ -5,7 +5,6 @@
5
5
  # LICENSE file in the root directory of this source tree.
6
6
 
7
7
  require 'cgi'
8
- require 'active_support/core_ext/hash/compact'
9
8
 
10
9
  require_relative 'profile'
11
10
  require_relative '../extensions/enumerable'
@@ -241,7 +240,7 @@ module GoodData
241
240
 
242
241
  all_users
243
242
  else
244
- find_user_by_login(domain, id)
243
+ find_user_by_login(domain, id, opts)
245
244
  end
246
245
  end
247
246
 
@@ -105,6 +105,7 @@ module GoodData
105
105
  d[:title] = date_dim['dateDimension']['title']
106
106
  d[:urn] = date_dim['dateDimension']['urn']
107
107
  d[:identifier_prefix] = date_dim['dateDimension']['identifierPrefix']
108
+ d[:identifier] = date_dim['dateDimension']['identifier'] if date_dim['dateDimension']['identifier']
108
109
  d[:columns] = parse_bridges(date_dim)
109
110
  end
110
111
  end
@@ -30,7 +30,7 @@ module GoodData
30
30
  :deprecated => 0,
31
31
 
32
32
  # Content When options
33
- :recurrency => '0:0:0:12:0:0',
33
+ :recurrency => '0:0:0:1*12:0:0',
34
34
  :startDate => '2012-06-05',
35
35
  :timeZone => 'America/Los_Angeles',
36
36
 
@@ -118,11 +118,13 @@ module GoodData
118
118
  GoodData.logger.info("Deploying #{path}") if verbose
119
119
 
120
120
  deployed_path = Process.upload_package(path, files_to_exclude, client: client, project: project)
121
+ data_sources = options[:data_sources] || []
121
122
  data = {
122
123
  :process => {
123
124
  :name => deploy_name,
124
125
  :path => "/uploads/#{File.basename(deployed_path)}",
125
- :type => type
126
+ :type => type,
127
+ :dataSources => data_sources
126
128
  }
127
129
  }
128
130
 
@@ -171,10 +173,12 @@ module GoodData
171
173
  verbose = options[:verbose] || false
172
174
  GoodData.logger.info("Deploying #{path}") if verbose
173
175
 
176
+ data_sources = options[:data_sources] || []
174
177
  data = {
175
178
  process: {
176
179
  name: deploy_name,
177
180
  path: path,
181
+ dataSources: data_sources,
178
182
  type: 'RUBY'
179
183
  }
180
184
  }
@@ -185,7 +189,7 @@ module GoodData
185
189
  def deploy_component(data, options = { client: GoodData.client, project: GoodData.project })
186
190
  client, project = GoodData.get_client_and_project(options)
187
191
  data = { process: data } unless data[:process]
188
- data[:process] = GoodData::Helpers.symbolize_keys(data[:process]).select { |k| %i[type name component].include? k }
192
+ data[:process] = GoodData::Helpers.symbolize_keys(data[:process]).select { |k| %i[type name component dataSources].include? k }
189
193
  data[:process][:component] = GoodData::Helpers.symbolize_keys(data[:process][:component]).select { |k| %i[name version configLocation config].include? k }
190
194
 
191
195
  save(data, options)
@@ -266,7 +270,7 @@ module GoodData
266
270
  # @option options [String] :name Readable name of the process
267
271
  # @option options [Boolean] :verbose (false) Switch on verbose mode for detailed logging
268
272
  def deploy(path, options = {})
269
- Process.deploy(path, { client: client, process_id: process_id, :project => project, :name => name, :type => type }.merge(options))
273
+ Process.deploy(path, { client: client, process_id: process_id, :project => project, :name => name, :type => type, :data_sources => data_sources }.merge(options))
270
274
  end
271
275
 
272
276
  # Downloads the process from S3 in a zipped form.
@@ -326,6 +330,10 @@ module GoodData
326
330
  process['component']
327
331
  end
328
332
 
333
+ def data_sources
334
+ process['dataSources']
335
+ end
336
+
329
337
  # Determines whether the process is an ADDv2 component.
330
338
  # @return [Bool] True if the process is an ADDv2 component.
331
339
  def add_v2_component?
@@ -1,6 +1,6 @@
1
1
  # encoding: UTF-8
2
2
  #
3
- # Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
3
+ # Copyright (c) 2010-2021 GoodData Corporation. All rights reserved.
4
4
  # This source code is licensed under the BSD-style license found in the
5
5
  # LICENSE file in the root directory of this source tree.
6
6
 
@@ -13,7 +13,6 @@ require 'zip'
13
13
  require 'net/smtp'
14
14
 
15
15
  require 'active_support/core_ext/hash/except'
16
- require 'active_support/core_ext/hash/compact'
17
16
  require 'active_support/core_ext/hash/slice'
18
17
 
19
18
  require_relative '../exceptions/no_project_error'
@@ -38,7 +37,8 @@ require_relative 'metadata/scheduled_mail/report_attachment'
38
37
 
39
38
  module GoodData
40
39
  class Project < Rest::Resource
41
- USERSPROJECTS_PATH = '/gdc/account/profile/%s/projects'
40
+ USER_ACCOUNT_PATH = '/gdc/account/profile/'
41
+ USERSPROJECTS_PATH = USER_ACCOUNT_PATH + '%s/projects'
42
42
  PROJECTS_PATH = '/gdc/projects'
43
43
  PROJECT_PATH = '/gdc/projects/%s'
44
44
  SLIS_PATH = '/ldm/singleloadinterface'
@@ -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,78 @@ 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::DataSource.from_id(data_source_id, client: 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] = 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 = 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
+
375
+ # Verify whether the data source exists in the domain using its alias
376
+ #
377
+ # @param [String] ds_alias The data source's alias
378
+ # @param [Object] client The Rest Client object
379
+ # @return [String] Id of the data source or failed with the reason
380
+ def verify_data_source_alias(ds_alias, client)
381
+ domain = client.connection.server.url
382
+ fail "The data source alias is empty, check your data source configuration." unless ds_alias
383
+
384
+ uri = "/gdc/dataload/dataSources/internal/availableAlias?alias=#{ds_alias[:alias]}"
385
+ res = client.get(uri)
386
+ fail "Unable to get information about the Data Source '#{ds_alias[:alias]}' in the domain '#{domain}'" unless res
387
+ fail "Unable to find the #{ds_alias[:type]} Data Source '#{ds_alias[:alias]}' in the domain '#{domain}'" if res['availableAlias']['available']
388
+
389
+ ds_type = res['availableAlias']['existingDataSource']['type']
390
+ if ds_type && ds_type != ds_alias[:type]
391
+ fail "Wrong Data Source type - the '#{ds_type}' type is expected but the Data Source '#{ds_alias[:alias]}' in the domain '#{domain}' has the '#{ds_alias[:type]}' type"
392
+ else
393
+ res['availableAlias']['existingDataSource']['id']
394
+ end
395
+ end
396
+
321
397
  def transfer_user_groups(from_project, to_project)
322
398
  from_project.user_groups.map do |ug|
323
399
  # migrate groups
@@ -625,6 +701,7 @@ module GoodData
625
701
  def blueprint(options = {})
626
702
  options = { include_ca: true }.merge(options)
627
703
  result = client.get("/gdc/projects/#{pid}/model/view", params: { includeDeprecated: true, includeGrain: true, includeCA: options[:include_ca] })
704
+
628
705
  polling_url = result['asyncTask']['link']['poll']
629
706
  model = client.poll_on_code(polling_url, options)
630
707
  bp = GoodData::Model::FromWire.from_wire(model, options)
@@ -1606,14 +1683,19 @@ module GoodData
1606
1683
  def import_users(new_users, options = {})
1607
1684
  role_list = roles
1608
1685
  users_list = users
1609
- 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! } }
1610
1686
 
1611
1687
  GoodData.logger.warn("Importing users to project (#{pid})")
1688
+ 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! } }
1689
+ # First check that if groups are provided we have them set up
1690
+ user_groups_cache, change_groups = check_groups(new_users.map(&:to_hash).flat_map { |u| u[:user_group] || [] }.uniq, options[:user_groups_cache], options)
1612
1691
 
1613
- whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1692
+ unless change_groups.empty?
1693
+ new_users.each do |user|
1694
+ user[:user_group].map! { |e| change_groups[e].nil? ? e : change_groups[e] }
1695
+ end
1696
+ end
1614
1697
 
1615
- # First check that if groups are provided we have them set up
1616
- user_groups_cache = check_groups(new_users.map(&:to_hash).flat_map { |u| u[:user_group] || [] }.uniq, options[:user_groups_cache], options)
1698
+ whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1617
1699
 
1618
1700
  # conform the role on list of new users so we can diff them with the users coming from the project
1619
1701
  diffable_new_with_default_role = whitelisted_new_users.map do |u|
@@ -1656,7 +1738,7 @@ module GoodData
1656
1738
  end
1657
1739
  end
1658
1740
  diff_results = diff_results.map do |u|
1659
- u[:login_uri] = "/gdc/account/profile/" + u[:login]
1741
+ u[:login_uri] = USER_ACCOUNT_PATH + u[:login]
1660
1742
  u
1661
1743
  end
1662
1744
  return diff_results if options[:dry_run]
@@ -1760,7 +1842,20 @@ module GoodData
1760
1842
  def check_groups(specified_groups, user_groups_cache = nil, options = {})
1761
1843
  current_user_groups = user_groups if user_groups_cache.nil? || user_groups_cache.empty?
1762
1844
  groups = current_user_groups.map(&:name)
1763
- missing_groups = specified_groups - groups
1845
+ missing_groups = []
1846
+ change_groups = {}
1847
+ specified_groups.each do |group|
1848
+ found_group = groups.find { |name| name.casecmp(group).zero? }
1849
+ if found_group.nil?
1850
+ missing_groups << group
1851
+ else
1852
+ # Change groups when they have similar group name with difference of case sensitivity
1853
+ if found_group != group
1854
+ change_groups[group] = found_group
1855
+ GoodData.logger.warn("Group with name #{group} is existed in project with name #{found_group}.")
1856
+ end
1857
+ end
1858
+ end
1764
1859
  if options[:create_non_existing_user_groups]
1765
1860
  missing_groups.each do |g|
1766
1861
  GoodData.logger.info("Creating group #{g}")
@@ -1773,7 +1868,7 @@ module GoodData
1773
1868
  "#{groups.join(',')} and you asked for #{missing_groups.join(',')}"
1774
1869
  end
1775
1870
  end
1776
- current_user_groups
1871
+ [current_user_groups, change_groups]
1777
1872
  end
1778
1873
 
1779
1874
  # Update user
@@ -1879,17 +1974,17 @@ module GoodData
1879
1974
 
1880
1975
  def resolve_roles(login, desired_roles, options = {})
1881
1976
  user = if login.is_a?(String) && login.include?('@')
1882
- '/gdc/account/profile/' + login
1977
+ USER_ACCOUNT_PATH + login
1883
1978
  elsif login.is_a?(String)
1884
1979
  login
1885
1980
  elsif login.is_a?(Hash) && login[:login]
1886
- '/gdc/account/profile/' + login[:login]
1981
+ USER_ACCOUNT_PATH + login[:login]
1887
1982
  elsif login.is_a?(Hash) && login[:uri]
1888
1983
  login[:uri]
1889
1984
  elsif login.respond_to?(:uri) && login.uri
1890
1985
  login.uri
1891
1986
  elsif login.respond_to?(:login) && login.login
1892
- '/gdc/account/profile/' + login.login
1987
+ USER_ACCOUNT_PATH + login.login
1893
1988
  else
1894
1989
  fail "Unsupported user specification #{login}"
1895
1990
  end
@@ -1904,6 +1999,20 @@ module GoodData
1904
1999
  [user, roles]
1905
2000
  end
1906
2001
 
2002
+ def upgrade_custom_v2(message, options = {})
2003
+ uri = "/gdc/md/#{pid}/datedimension/upgrade"
2004
+ poll_result = client&.post(uri, message)
2005
+
2006
+ return poll_result['wTaskStatus']['status'] if poll_result['wTaskStatus'] && poll_result['wTaskStatus']['status']
2007
+
2008
+ polling_uri = poll_result['asyncTask']['link']['poll']
2009
+ result = client&.poll_on_response(polling_uri, options) do |body|
2010
+ body && body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
2011
+ end
2012
+
2013
+ result['wTaskStatus']['status'] == 'OK' ? 'OK' : 'FAIL'
2014
+ end
2015
+
1907
2016
  def add
1908
2017
  @add ||= GoodData::AutomatedDataDistribution.new(self)
1909
2018
  @add
@@ -7,7 +7,6 @@
7
7
  require_relative '../project_log_formatter'
8
8
 
9
9
  require 'active_support/core_ext/hash/indifferent_access'
10
- require 'active_support/core_ext/hash/compact'
11
10
 
12
11
  require 'gooddata/extensions/true'
13
12
  require 'gooddata/extensions/false'
@@ -13,7 +13,6 @@ require_relative '../mixins/rest_resource'
13
13
  require_relative '../mixins/uri_getter'
14
14
 
15
15
  require 'active_support/core_ext/hash/except'
16
- require 'active_support/core_ext/hash/compact'
17
16
 
18
17
  module GoodData
19
18
  # Representation of User Group
@@ -1,6 +1,6 @@
1
1
  # encoding: UTF-8
2
2
  #
3
- # Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
3
+ # Copyright (c) 2010-2021 GoodData Corporation. All rights reserved.
4
4
  # This source code is licensed under the BSD-style license found in the
5
5
  # LICENSE file in the root directory of this source tree.
6
6
 
@@ -26,6 +26,8 @@ module GoodData
26
26
  class Connection
27
27
  include MonitorMixin
28
28
 
29
+ HTTPS_PROTOCOL = 'https://'
30
+ HTTP_PROTOCOL = 'http://'
29
31
  DEFAULT_URL = 'https://secure.gooddata.com'
30
32
  LOGIN_PATH = '/gdc/account/login'
31
33
  TOKEN_PATH = '/gdc/account/token'
@@ -712,12 +714,12 @@ ERR
712
714
 
713
715
  def fix_server_url(server)
714
716
  server = server.chomp('/')
715
- if server.start_with? 'http://'
716
- server = server.sub 'http://', 'https://'
717
+ if server.start_with? HTTP_PROTOCOL
718
+ server = server.sub HTTP_PROTOCOL, HTTPS_PROTOCOL
717
719
  GoodData.logger.warn 'You specified the HTTP protocol in your server string. It has been autofixed to HTTPS.'
718
720
  end
719
721
 
720
- server = 'https://' + server unless server.start_with? 'https://'
722
+ server = HTTPS_PROTOCOL + server unless server.start_with? HTTPS_PROTOCOL
721
723
  server
722
724
  end
723
725
  end
@@ -1,51 +1,51 @@
1
- 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
- NkWh+M+oKVIdQiNR+arCaMiQMlxMGd0v00f8+A1jpI7tWy/tnCO7Nicmqoy8
3
- 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
- 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
- 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
- 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
- pVJC7ecS9LhO1I8L3cxSIBUW0UM8paoXy4XCvxeLtClGhU3gHCfHR9BE19D8
8
- 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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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
- 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
- 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
- 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
- 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
- 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
- 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
- foQbhplaZc3qb/lHhhppVHx6VG6bJX4eePm9sBNHB6brEZjUKJ/Y7wrDTeVo
26
- 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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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
- 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
- +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
- bi1yTPovLT0HUYb5sRxuk/QV5YuoGy04bHoT5+beohnM5UjsKVIZsJ4XYqNM
40
- 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
- afeDOKRK8BjBmgzElGiOiLcc+RrvW6f61qLPyWC2nJ9THdwmYFaMurGrM6ed
42
- j+ZkUTdCFOnD88sbtUQ1kAKs44vggWmYitUqdyI6ZmReGj9kmbAu3NHZ7c6g
43
- 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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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