superset 0.1.6

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 +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