superset 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.buildkite/pipeline.yml +16 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +13 -0
  5. data/CHANGELOG.md +48 -0
  6. data/Dockerfile +17 -0
  7. data/LICENSE +21 -0
  8. data/README.md +205 -0
  9. data/Rakefile +12 -0
  10. data/doc/duplicate_dashboards.md +214 -0
  11. data/doc/setting_up_personal_api_credentials.md +127 -0
  12. data/docker-compose.override.yml +10 -0
  13. data/docker-compose.yml +8 -0
  14. data/env.sample +9 -0
  15. data/lib/loggers/duplicate_dashboard_logger.rb +15 -0
  16. data/lib/superset/authenticator.rb +55 -0
  17. data/lib/superset/chart/bulk_delete.rb +40 -0
  18. data/lib/superset/chart/delete.rb +30 -0
  19. data/lib/superset/chart/get.rb +56 -0
  20. data/lib/superset/chart/list.rb +59 -0
  21. data/lib/superset/chart/update_dataset.rb +90 -0
  22. data/lib/superset/client.rb +53 -0
  23. data/lib/superset/credential/api_user.rb +25 -0
  24. data/lib/superset/credential/embedded_user.rb +25 -0
  25. data/lib/superset/dashboard/bulk_delete.rb +42 -0
  26. data/lib/superset/dashboard/bulk_delete_cascade.rb +52 -0
  27. data/lib/superset/dashboard/charts/list.rb +47 -0
  28. data/lib/superset/dashboard/compare.rb +94 -0
  29. data/lib/superset/dashboard/copy.rb +78 -0
  30. data/lib/superset/dashboard/datasets/list.rb +74 -0
  31. data/lib/superset/dashboard/delete.rb +42 -0
  32. data/lib/superset/dashboard/embedded/get.rb +56 -0
  33. data/lib/superset/dashboard/embedded/put.rb +35 -0
  34. data/lib/superset/dashboard/export.rb +98 -0
  35. data/lib/superset/dashboard/get.rb +51 -0
  36. data/lib/superset/dashboard/info.rb +17 -0
  37. data/lib/superset/dashboard/list.rb +99 -0
  38. data/lib/superset/dashboard/put.rb +37 -0
  39. data/lib/superset/dashboard/warm_up_cache.rb +42 -0
  40. data/lib/superset/database/get.rb +30 -0
  41. data/lib/superset/database/get_schemas.rb +25 -0
  42. data/lib/superset/database/list.rb +51 -0
  43. data/lib/superset/dataset/bulk_delete.rb +41 -0
  44. data/lib/superset/dataset/create.rb +62 -0
  45. data/lib/superset/dataset/delete.rb +30 -0
  46. data/lib/superset/dataset/duplicate.rb +62 -0
  47. data/lib/superset/dataset/get.rb +56 -0
  48. data/lib/superset/dataset/list.rb +41 -0
  49. data/lib/superset/dataset/update_query.rb +56 -0
  50. data/lib/superset/dataset/update_schema.rb +120 -0
  51. data/lib/superset/dataset/warm_up_cache.rb +41 -0
  52. data/lib/superset/display.rb +42 -0
  53. data/lib/superset/enumerations/object_type.rb +11 -0
  54. data/lib/superset/file_utilities.rb +19 -0
  55. data/lib/superset/guest_token.rb +69 -0
  56. data/lib/superset/logger.rb +20 -0
  57. data/lib/superset/request.rb +62 -0
  58. data/lib/superset/route_info.rb +34 -0
  59. data/lib/superset/security/permissions_resources/list.rb +22 -0
  60. data/lib/superset/security/role/create.rb +25 -0
  61. data/lib/superset/security/role/get.rb +32 -0
  62. data/lib/superset/security/role/list.rb +45 -0
  63. data/lib/superset/security/role/permission/create.rb +35 -0
  64. data/lib/superset/security/role/permission/get.rb +37 -0
  65. data/lib/superset/security/user/create.rb +49 -0
  66. data/lib/superset/security/user/get.rb +27 -0
  67. data/lib/superset/security/user/list.rb +42 -0
  68. data/lib/superset/services/duplicate_dashboard.rb +298 -0
  69. data/lib/superset/sqllab/execute.rb +52 -0
  70. data/lib/superset/tag/add_to_object.rb +46 -0
  71. data/lib/superset/tag/get.rb +30 -0
  72. data/lib/superset/tag/list.rb +37 -0
  73. data/lib/superset/version.rb +5 -0
  74. data/lib/superset.rb +17 -0
  75. data/log/README.md +4 -0
  76. data/superset.gemspec +55 -0
  77. metadata +300 -0
@@ -0,0 +1,120 @@
1
+ module Superset
2
+ module Dataset
3
+ class UpdateSchema < Superset::Request
4
+
5
+ attr_reader :source_dataset_id, :target_database_id, :target_schema, :remove_copy_suffix
6
+
7
+ def initialize(source_dataset_id: , target_database_id: , target_schema: , remove_copy_suffix: false)
8
+ @source_dataset_id = source_dataset_id
9
+ @target_database_id = target_database_id
10
+ @target_schema = target_schema
11
+ @remove_copy_suffix = remove_copy_suffix
12
+ end
13
+
14
+ def perform
15
+ validate_proposed_changes
16
+
17
+ response
18
+
19
+ msg = if result['schema'] == target_schema
20
+ "Successfully updated dataset schema to #{target_schema} on Database: #{target_database_id}"
21
+ else
22
+ "Error: Failed to update dataset schema to #{target_schema} on Database: #{target_database_id}"
23
+ end
24
+
25
+ logger.info " #{msg}"
26
+ msg
27
+
28
+ end
29
+
30
+ def response
31
+ @response ||= client.put(route, params_updated)
32
+ end
33
+
34
+ def params_updated
35
+ @params_updated ||= begin
36
+ new_params = source_dataset.slice(*acceptable_attributes).with_indifferent_access
37
+
38
+ # primary database and schema changes
39
+ new_params.merge!("database_id": target_database_id) # add the target database id
40
+ new_params['schema'] = target_schema
41
+ new_params['owners'] = new_params['owners'].map {|o| o['id'] } # expects an array of user ids
42
+ new_params['table_name'] = new_params['table_name'].gsub(/ \(COPY\)/, '') if remove_copy_suffix
43
+
44
+ # remove unwanted fields from metrics and columns arrays
45
+ new_params['metrics'].each {|m| m.delete('changed_on') }
46
+ new_params['metrics'].each {|m| m.delete('created_on') }
47
+ new_params['columns'].each {|m| m.delete('changed_on') }
48
+ new_params['columns'].each {|m| m.delete('created_on') }
49
+ new_params['columns'].each {|m| m.delete('type_generic') }
50
+ new_params
51
+ end
52
+ end
53
+
54
+ # check if the sql query embedds the schema name, if so it can not be duplicated cleanly
55
+ def sql_query_includes_hard_coded_schema?
56
+ source_dataset['sql'].include?("#{source_dataset['schema']}.")
57
+ end
58
+
59
+ private
60
+
61
+ def source_dataset
62
+ # will raise an error if the dataset does not exist
63
+ @source_dataset ||= begin
64
+ dataset = Get.new(source_dataset_id)
65
+ dataset.result
66
+ end
67
+ end
68
+
69
+ def validate_proposed_changes
70
+ logger.info " Validating Dataset ID: #{source_dataset_id} schema update to #{target_schema} on Database: #{target_database_id}"
71
+ raise "Error: source_dataset_id integer is required" unless source_dataset_id.present? && source_dataset_id.is_a?(Integer)
72
+ raise "Error: target_database_id integer is required" unless target_database_id.present? && target_database_id.is_a?(Integer)
73
+ raise "Error: target_schema string is required" unless target_schema.present? && target_schema.is_a?(String)
74
+
75
+ # confirm the dataset exist? ... no need as the load_source_dataset method will raise an error if the dataset does not exist
76
+
77
+ # does the target schema exist in the target database?
78
+ raise "Error: Schema #{target_schema} does not exist in database: #{target_database_id}" unless target_database_available_schemas.include?(target_schema)
79
+
80
+ # does the sql query hard code the current schema name?
81
+ raise "Error: >>WARNING<< The Dataset ID #{source_dataset_id} SQL query is hard coded with the schema value and can not be duplicated cleanly. " +
82
+ "Remove all direct embedded schema calls from the Dataset SQL query before continuing." if sql_query_includes_hard_coded_schema?
83
+ end
84
+
85
+ # attrs as per swagger docs for dataset patch
86
+ def acceptable_attributes
87
+ %w(
88
+ always_filter_main_dttm
89
+ cache_timeout
90
+ columns
91
+ database_id
92
+ default_endpoint
93
+ description
94
+ extra
95
+ fetch_values_predicate
96
+ filter_select_enabled
97
+ is_managed_externally
98
+ is_sqllab_view
99
+ main_dttm_col
100
+ metrics
101
+ normalize_columns
102
+ offset
103
+ owners
104
+ schema
105
+ sql
106
+ table_name
107
+ template_params
108
+ )
109
+ end
110
+
111
+ def route
112
+ "dataset/#{source_dataset_id}"
113
+ end
114
+
115
+ def target_database_available_schemas
116
+ Superset::Database::GetSchemas.call(target_database_id)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,41 @@
1
+ module Superset
2
+ module Dataset
3
+ class WarmUpCache < Superset::Request
4
+
5
+ attr_reader :dashboard_id, :table_name, :db_name
6
+
7
+ def initialize(dashboard_id:, table_name:, db_name:)
8
+ @dashboard_id = dashboard_id
9
+ @table_name = table_name
10
+ @db_name = db_name
11
+ end
12
+
13
+ def perform
14
+ response
15
+ end
16
+
17
+ def response
18
+ logger.info("Hitting #{route} for warming up the cache for the dashboard #{dashboard_id.to_s} and for the dataset #{table_name}")
19
+ client.put(route, params(dashboard_id, table_name, db_name))
20
+ end
21
+
22
+ def params(dashboard_id, table_name, db_name)
23
+ {
24
+ "dashboard_id" => dashboard_id,
25
+ "table_name" => table_name,
26
+ "db_name" => db_name
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def route
33
+ "dataset/warm_up_cache"
34
+ end
35
+
36
+ def logger
37
+ @logger ||= Superset::Logger.new
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,42 @@
1
+ module Superset
2
+ module Display
3
+ def list
4
+ puts table.to_s
5
+ end
6
+
7
+ def table
8
+ Terminal::Table.new(
9
+ title: title,
10
+ headings: headings,
11
+ rows: rows
12
+ )
13
+ end
14
+
15
+ def rows
16
+ result.map do |d|
17
+ list_attributes.map { |la| d[la].to_s }
18
+ end
19
+ end
20
+
21
+ def title
22
+ self.class.to_s
23
+ end
24
+
25
+ def headings
26
+ headings = display_headers ? display_headers : list_attributes
27
+ headings.map(&:to_s).map(&:humanize)
28
+ end
29
+
30
+ def display_headers
31
+ # optionally override this method to display custom headers
32
+ end
33
+
34
+ def list_attributes
35
+ raise NotImplementedError.new("You must implement list_attributes.")
36
+ end
37
+
38
+ def result
39
+ raise NotImplementedError.new("You must implement result.")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ class ObjectType < EnumerateIt::Base
2
+ # replicates the Superset Core code base enum for Object Types
3
+ # https://github.com/apache/superset/blob/40e77be813c789c8b01aece739f32ff5753436b4/superset/tags/models.py#L79
4
+
5
+ associate_values(
6
+ query: 1,
7
+ chart: 2,
8
+ dashboard: 3,
9
+ dataset: 4
10
+ )
11
+ end
@@ -0,0 +1,19 @@
1
+ require 'zip'
2
+
3
+ module Superset
4
+ module FileUtilities
5
+ def unzip_file(zip_file, destination)
6
+ entries = []
7
+ Zip::File.open(zip_file) do |zip|
8
+ zip.each do |entry|
9
+ entry_path = File.join(destination, entry.name)
10
+ entries << entry_path
11
+ FileUtils.mkdir_p(File.dirname(entry_path))
12
+ zip.extract(entry, entry_path)
13
+ end
14
+ end
15
+ puts entries
16
+ entries # return array of extracted files
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,69 @@
1
+ module Superset
2
+ class GuestToken
3
+ include Credential::EmbeddedUser
4
+
5
+ attr_accessor :embedded_dashboard_id, :current_user
6
+
7
+ def initialize(embedded_dashboard_id: , current_user: nil)
8
+ @embedded_dashboard_id = embedded_dashboard_id
9
+ @current_user = current_user
10
+ end
11
+
12
+ def guest_token
13
+ response_body['token']
14
+ end
15
+
16
+ def params
17
+ {
18
+ "resources": [
19
+ {
20
+ "id": embedded_dashboard_id.to_s,
21
+ "type": "dashboard" }
22
+ ],
23
+ "rls": [],
24
+ "user": current_user_params
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ # optional param to be available in Superset for query templating using jinja
31
+ # ss expects username .. which could be used to query as current_user.id
32
+ def current_user_params
33
+ if current_user
34
+ { "username": current_user.id.to_s }
35
+ else
36
+ { }
37
+ end
38
+ end
39
+
40
+ def response_body
41
+ response.env.body
42
+ end
43
+
44
+ def route
45
+ 'api/v1/security/guest_token/'
46
+ end
47
+
48
+ def response
49
+ @response ||= connection.post(route, params.to_json)
50
+ end
51
+
52
+ def connection
53
+ @connection ||= Faraday.new(authenticator.superset_host) do |f|
54
+ f.authorization :Bearer, access_token
55
+ f.use FaradayMiddleware::ParseJson, content_type: 'application/json'
56
+ f.request :json
57
+ f.adapter :net_http
58
+ end
59
+ end
60
+
61
+ def access_token
62
+ @access_token ||= authenticator.access_token
63
+ end
64
+
65
+ def authenticator
66
+ @authenticator ||= Superset::Authenticator.new(credentials)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,20 @@
1
+ module Superset
2
+ class Logger
3
+
4
+ def info(msg)
5
+ # puts msg # allow logs to console
6
+ logger.info msg
7
+ end
8
+
9
+ def error(msg)
10
+ # puts msg # allow logs to console
11
+ logger.error msg
12
+ end
13
+
14
+ def logger
15
+ @logger ||= begin
16
+ ::Logger.new("log/superset-client.log")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,62 @@
1
+ module Superset
2
+ class Request
3
+ include Display
4
+
5
+ class InvalidParameterError < StandardError; end
6
+ class ValidationError < StandardError; end
7
+
8
+
9
+ PAGE_SIZE = 100
10
+
11
+ attr_accessor :page_num
12
+
13
+ def initialize(page_num: 0)
14
+ @page_num = page_num
15
+ end
16
+
17
+ def self.call
18
+ self.new.response
19
+ end
20
+
21
+ def response
22
+ @response ||= client.get(route)
23
+ rescue => e
24
+ logger.error("#{e.message}")
25
+ raise e
26
+ end
27
+
28
+ def result
29
+ response['result']
30
+ end
31
+
32
+ def superset_host
33
+ client.superset_host
34
+ end
35
+
36
+ def query_params
37
+ [filters, pagination].join
38
+ end
39
+
40
+ private
41
+
42
+ def route
43
+ raise NotImplementedError.new("You must implement route.")
44
+ end
45
+
46
+ def client
47
+ @client ||= Superset::Client.new
48
+ end
49
+
50
+ def pagination
51
+ "page:#{page_num},page_size:#{PAGE_SIZE}"
52
+ end
53
+
54
+ def filters
55
+ ""
56
+ end
57
+
58
+ def logger
59
+ @logger ||= Superset::Logger.new
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ module Superset
2
+ class RouteInfo < Superset::Request
3
+ alias result response
4
+
5
+ attr_reader :route
6
+
7
+ def initialize(route:)
8
+ @route = route
9
+ end
10
+
11
+ def perform
12
+ validate_route
13
+ response
14
+ end
15
+
16
+ def response
17
+ validate_route
18
+ @response ||= client.get(route)
19
+ end
20
+
21
+ def filters
22
+ result['filters']
23
+ end
24
+
25
+ private
26
+
27
+ def validate_route
28
+ unless route.present? && route.is_a?(String)
29
+ puts "Example Route: 'dashboard/_info' "
30
+ raise "Error: route string is required" unless route.present? && route.is_a?(String)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ module Superset
2
+ module Security
3
+ module PermissionsResources
4
+ class List < Superset::Request
5
+
6
+ def initialize(page_num: 0)
7
+ super(page_num: page_num)
8
+ end
9
+
10
+ private
11
+
12
+ def list_attributes
13
+ [:id, :permission, :view_menu]
14
+ end
15
+
16
+ def route
17
+ "security/permissions-resources/?q=(#{pagination})"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module Superset
2
+ module Security
3
+ module Role
4
+ class Create < Superset::Request
5
+ attr_reader :name
6
+
7
+ def initialize(name: '')
8
+ @name = name
9
+ end
10
+
11
+ def response
12
+ raise InvalidParameterError unless name.present?
13
+
14
+ @response ||= client.post(route, { 'name' => name } )
15
+ end
16
+
17
+ private
18
+
19
+ def route
20
+ "security/roles/"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ module Superset
2
+ module Security
3
+ module Role
4
+ class Get < Superset::Request
5
+ attr_reader :id
6
+
7
+ def initialize(id)
8
+ @id = id
9
+ end
10
+
11
+ def result
12
+ [ super ]
13
+ end
14
+
15
+ def id_and_name
16
+ result.first.slice(:id, :name).values.join(': ')
17
+ end
18
+
19
+ private
20
+
21
+ def list_attributes
22
+ [:id, :name]
23
+ end
24
+
25
+ def route
26
+ "security/roles/#{id}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,45 @@
1
+ module Superset
2
+ module Security
3
+ module Role
4
+ class List < Superset::Request
5
+ attr_reader :name_contains, :name_equals
6
+
7
+ def initialize(page_num: 0, name_contains: nil, name_equals: nil)
8
+ @name_contains= name_contains
9
+ @name_equals= name_equals
10
+ super(page_num: page_num)
11
+ end
12
+
13
+ def query_params
14
+ [filters, pagination].join
15
+ end
16
+
17
+ private
18
+
19
+ def route
20
+ "security/roles/?q=(#{query_params})"
21
+ end
22
+
23
+ def filters
24
+ raise 'ERROR: only one filter supported currently' if name_contains.present? && name_equals.present?
25
+
26
+ if name_contains.present?
27
+ "filters:!((col:name,opr:ct,value:'#{name_contains}')),"
28
+ elsif name_equals.present?
29
+ "filters:!((col:name,opr:eq,value:'#{name_equals}')),"
30
+ else
31
+ ''
32
+ end
33
+ end
34
+
35
+ def title
36
+ "#{response[:count]} Roles for Host: #{superset_host}"
37
+ end
38
+
39
+ def list_attributes
40
+ [:id, :name]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ module Superset
2
+ module Security
3
+ module Role
4
+ module Permission
5
+ class Create < Superset::Request
6
+ attr_reader :role_id, :permission_view_menu_ids
7
+
8
+ def initialize(role_id:, permission_view_menu_ids: [])
9
+ @permission_view_menu_ids = permission_view_menu_ids
10
+ @role_id = role_id
11
+ end
12
+
13
+ def response
14
+ raise InvalidParameterError unless valid_params?
15
+
16
+ @response ||= client.post(route,
17
+ { "permission_view_menu_ids": permission_view_menu_ids } )
18
+ end
19
+
20
+ private
21
+
22
+ def valid_params?
23
+ role_id.present? &&
24
+ permission_view_menu_ids.is_a?(Array) &&
25
+ !permission_view_menu_ids.empty?
26
+ end
27
+
28
+ def route
29
+ "security/roles/#{role_id}/permissions"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ module Superset
2
+ module Security
3
+ module Role
4
+ module Permission
5
+ class Get < Superset::Request
6
+ attr_reader :id
7
+
8
+ def initialize(id)
9
+ @id = id
10
+ end
11
+
12
+ def self.call(id)
13
+ self.new(id)
14
+ end
15
+
16
+ def result
17
+ response[:result]
18
+ end
19
+
20
+ private
21
+
22
+ def list_attributes
23
+ [:id, :permission_name, :view_menu_name]
24
+ end
25
+
26
+ def route
27
+ "security/roles/#{id}/permissions/"
28
+ end
29
+
30
+ def title
31
+ Superset::Security::Role::Get.new(id).id_and_name
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,49 @@
1
+ module Superset
2
+ module Security
3
+ module User
4
+ class Create < Superset::Request
5
+ attr_reader :user_params
6
+
7
+ def initialize(user_params: {})
8
+ @user_params = user_params.with_indifferent_access
9
+ end
10
+
11
+ def response
12
+ validate_user_params
13
+
14
+ @response ||= client.post(route, user_params)
15
+ end
16
+
17
+ def validate_user_params
18
+ raise InvalidParameterError, "Missing user params. Expects #{valid_user_params_keys}" unless symbolized_user_param_keys == valid_user_params_keys
19
+ raise InvalidParameterError, 'Roles must be an array ' unless user_params[:roles].is_a?(Array)
20
+ confirm_all_params_present
21
+ end
22
+
23
+ private
24
+
25
+ def error_message
26
+ errors = ''
27
+ end
28
+
29
+ def confirm_all_params_present
30
+ symbolized_user_param_keys.each do |key|
31
+ raise InvalidParameterError, "Missing #{key}" unless user_params[key].present?
32
+ end
33
+ end
34
+
35
+ def symbolized_user_param_keys
36
+ user_params.keys.map(&:to_sym)
37
+ end
38
+
39
+ def valid_user_params_keys
40
+ [ :active, :email, :first_name, :last_name, :password, :roles, :username ]
41
+ end
42
+
43
+ def route
44
+ "security/users/"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end