power-bi 2.3.0 → 2.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c120eeef2ac35b02afcfff32690f003efad7fbfd59c7f2bd714a6d0659b1ed44
4
- data.tar.gz: ee077e4fa8f6faae52ad5c1107fd96f9cb8bae5e7c8f0ef47a83009e7c80ce0d
3
+ metadata.gz: 98d26348ce002789d1f065e91735ae2d441a64bb32c8c935993961036f2d2379
4
+ data.tar.gz: e3527606c13a8f62a4918b98e0dfc5ed441f691b836b4f670251e80442fcf104
5
5
  SHA512:
6
- metadata.gz: c9c742362808fb3fa08ad663ebbdaf34d916f932fa5bed8f66d657caf769c230a7694d82cd05555a642d41098ccb9fcf6eed9b6f89233ca344ff3f1c3d02b6fb
7
- data.tar.gz: dc28a5e89d26365b7b48d5f6538bd1e8c12a9fe45c81b4f216ae63074ced3b728f31395c98751ee571a9e7234315693af5f6a7cffe4d86c398f6ec6683cfeafe
6
+ metadata.gz: 5e1674d67c5cf55473e0325601b65b532dc374cd9e5a08a2345a282c4b5dd0a984392d1c0196857a649a00ceaf45362d21980e85c9050868b5538b4377f20dd6
7
+ data.tar.gz: 1c7517a30c1d32ddcc7e6d6883ef6a5b91fcac12500c76b89bdbebb473c405dde6b58f45a2b60dc3c60b7dfac440973ea81daa6a49f308c9d22b23670484293a
data/README.md CHANGED
@@ -62,7 +62,9 @@ Once authenticated, you can set profiles like this:
62
62
  pbi.profile = profile
63
63
  ```
64
64
 
65
- Every action executed after setting the profile, will be executed _through the eyes of the profile_. This way, you can create an isolated multi-tenant setup. Using profiles, simplifies the internal organization of Power BI and allows faster interaction with Power BI. This also lifts the 1000-workspaces limit that is imposed on Master Userss and Service Principals
65
+ Every action executed after setting the profile, will be executed _through the eyes of the profile_. This way, you can create an isolated multi-tenant setup. Using profiles, simplifies the internal organization of Power BI and allows faster interaction with Power BI. This also lifts the 1000-workspaces limit that is imposed on Master Users and Service Principals
66
+
67
+ Note: when working with Service principal profiles (SPP), you need to add the SPP to the gateway datasource before binding the gateway datasource to the dataset.
66
68
 
67
69
  # Supported endpoints
68
70
 
@@ -122,6 +124,11 @@ Note 2: to limit the number of API calls, it is best to directly use the _getter
122
124
  * Create a new gateway datasource: `gateway.gateway_datasource.create(name, credentials, db_server, db_name)`
123
125
  * Delete a new gateway datasource: `gateway_datasource.delete`
124
126
 
127
+ ## Gateway datasource users
128
+
129
+ * List datasource users in a gateway datasource: `gateway_datasource.gateway_datasource_users`
130
+ * Add a Service principal profile to a gateway datasource: `gateway_datasource.add_service_principal_profile_user(profile_id, principal_object_id)`
131
+
125
132
  ## Capacities
126
133
 
127
134
  Note: Capacities are Azure creatures, you can't create them in Power BI.
@@ -0,0 +1,80 @@
1
+ module PowerBI
2
+ class Admin
3
+
4
+ def initialize(tenant)
5
+ @tenant = tenant
6
+ end
7
+
8
+ def get_user_artifact_access(user_id, artifact_types: nil)
9
+ if artifact_types
10
+ params = {artifactTypes: artifact_types.join(',')}
11
+ else
12
+ params = {}
13
+ end
14
+
15
+ url = "/admin/users/#{user_id}/artifactAccess"
16
+
17
+ resp = @tenant.get(url, params)
18
+ data = resp[:ArtifactAccessEntities]
19
+
20
+ continuation_token = resp[:continuationToken]
21
+
22
+ while continuation_token
23
+ params = {continuationToken: "'#{continuation_token}'"}
24
+ resp = @tenant.get(url, params)
25
+ data += resp[:ArtifactAccessEntities]
26
+ continuation_token = resp[:continuationToken] ? URI::decode_uri_component(resp[:continuationToken]) : nil
27
+ end
28
+
29
+ data
30
+ end
31
+
32
+ def get_workspaces(filter: nil, expand: nil)
33
+ params = {}
34
+ params[:$filter] = filter if filter
35
+ params[:$expand] = expand if expand
36
+
37
+ url = '/admin/groups'
38
+
39
+ nr_records = 5000
40
+ count = 0
41
+
42
+ data = []
43
+
44
+ loop do
45
+ params[:$top] = nr_records
46
+ params[:$skip] = count
47
+ resp = @tenant.get(url, params)
48
+ data += resp[:value]
49
+ batch_count = resp[:value].size
50
+ count += batch_count
51
+ break if batch_count < nr_records
52
+ end
53
+
54
+ data
55
+ end
56
+
57
+ def force_delete_workspace_by_workspace_name(user_email, workspace_name)
58
+ workspace = get_workspaces(filter: "name eq '#{workspace_name}' and state eq 'Active'").first
59
+ add_user(user_email, workspace[:id], access_right: "Admin")
60
+ @tenant.workspace(workspace[:id]).delete
61
+ end
62
+
63
+ def force_delete_workspace_by_workspace_id(user_email, workspace_id)
64
+ add_user(user_email, workspace_id, access_right: "Admin")
65
+ @tenant.workspace(workspace_id).delete
66
+ end
67
+
68
+ private
69
+
70
+ def add_user(user_email, workspace_id, access_right: "Admin")
71
+ @tenant.post("/admin/groups/#{workspace_id}/users") do |req|
72
+ req.body = {
73
+ emailAddress: user_email,
74
+ groupUserAccessRight: access_right,
75
+ }.to_json
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -1,6 +1,6 @@
1
1
  module PowerBI
2
2
  class Dataset < Object
3
- attr_reader :workspace, :datasources, :parameters, :refresh_history
3
+ attr_reader :workspace, :datasources, :parameters
4
4
 
5
5
  def initialize(tenant, parent, id = nil)
6
6
  super(tenant, id)
@@ -38,6 +38,11 @@ module PowerBI
38
38
  true
39
39
  end
40
40
 
41
+ def refresh_history(entries_to_load = 1)
42
+ @refresh_history.entries_to_load = entries_to_load
43
+ @refresh_history
44
+ end
45
+
41
46
  def last_refresh
42
47
  @refresh_history.first
43
48
  end
@@ -8,7 +8,7 @@ module PowerBI
8
8
  end
9
9
 
10
10
  def get_data(id)
11
- @tenant.get("/gateways/#{id}")
11
+ @tenant.get("/gateways/#{id}", use_profile: false)
12
12
  end
13
13
 
14
14
  def data_to_attributes(data)
@@ -32,7 +32,7 @@ module PowerBI
32
32
  end
33
33
 
34
34
  def get_data
35
- @tenant.get("/gateways")[:value]
35
+ @tenant.get("/gateways", use_profile: false)[:value]
36
36
  end
37
37
  end
38
38
  end
@@ -1,14 +1,15 @@
1
1
  module PowerBI
2
2
  class GatewayDatasource < Object
3
- attr_reader :gateway
3
+ attr_reader :gateway, :gateway_datasource_users
4
4
 
5
5
  def initialize(tenant, parent, id = nil)
6
6
  super(tenant, id)
7
7
  @gateway = parent
8
+ @gateway_datasource_users = GatewayDatasourceUserArray.new(@tenant, self)
8
9
  end
9
10
 
10
11
  def get_data(id)
11
- @tenant.get("/gateways/#{@gateway.id}/datasources/#{id}")
12
+ @tenant.get("/gateways/#{@gateway.id}/datasources/#{id}", use_profile: false)
12
13
  end
13
14
 
14
15
  def data_to_attributes(data)
@@ -24,7 +25,7 @@ module PowerBI
24
25
  end
25
26
 
26
27
  def update_credentials(encrypted_credentials)
27
- @tenant.patch("/gateways/#{gateway.id}/datasources/#{id}") do |req|
28
+ @tenant.patch("/gateways/#{gateway.id}/datasources/#{id}", use_profile: false) do |req|
28
29
  req.body = {
29
30
  credentialDetails: {
30
31
  credentialType: "Basic",
@@ -41,7 +42,7 @@ module PowerBI
41
42
  end
42
43
 
43
44
  def delete
44
- @tenant.delete("/gateways/#{gateway.id}/datasources/#{id}")
45
+ @tenant.delete("/gateways/#{gateway.id}/datasources/#{id}", use_profile: false)
45
46
  @gateway.gateway_datasources.reload
46
47
  true
47
48
  end
@@ -61,7 +62,7 @@ module PowerBI
61
62
 
62
63
  # only MySQL type is currently supported
63
64
  def create(name, encrypted_credentials, db_server, db_name)
64
- data = @tenant.post("/gateways/#{@gateway.id}/datasources",) do |req|
65
+ data = @tenant.post("/gateways/#{@gateway.id}/datasources", use_profile: false) do |req|
65
66
  req.body = {
66
67
  connectionDetails: {server: db_server, database: db_name}.to_json,
67
68
  credentialDetails: {
@@ -82,7 +83,7 @@ module PowerBI
82
83
  end
83
84
 
84
85
  def get_data
85
- @tenant.get("/gateways/#{@gateway.id}/datasources")[:value]
86
+ @tenant.get("/gateways/#{@gateway.id}/datasources", use_profile: false)[:value]
86
87
  end
87
88
  end
88
89
  end
@@ -0,0 +1,51 @@
1
+ module PowerBI
2
+ class GatewayDatasourceUser < Object
3
+ attr_reader :gateway_datasource
4
+
5
+ def initialize(tenant, parent, id = nil)
6
+ super(tenant, id)
7
+ @gateway_datasource = parent
8
+ end
9
+
10
+ def data_to_attributes(data)
11
+ {
12
+ datasource_access_right: data[:datasourceAccessRight],
13
+ display_name: data[:displayName],
14
+ email_address: data[:emailAddress],
15
+ identifier: data[:identifier],
16
+ principal_type: data[:principalType],
17
+ profile: data[:profile],
18
+ }
19
+ end
20
+
21
+ end
22
+
23
+ class GatewayDatasourceUserArray < Array
24
+
25
+ def initialize(tenant, gateway_datasource)
26
+ super(tenant, gateway_datasource)
27
+ @gateway_datasource = gateway_datasource
28
+ end
29
+
30
+ def self.get_class
31
+ GatewayDatasourceUser
32
+ end
33
+
34
+ # service principal object ID: https://learn.microsoft.com/en-us/power-bi/developer/embedded/embedded-troubleshoot#what-is-the-difference-between-application-object-id-and-principal-object-id
35
+ def add_service_principal_profile_user(profile_id, principal_object_id, datasource_access_right: "Read")
36
+ @tenant.post("/gateways/#{@gateway_datasource.gateway.id}/datasources/#{@gateway_datasource.id}/users", use_profile: false) do |req|
37
+ req.body = {
38
+ datasourceAccessRight: datasource_access_right,
39
+ identifier: principal_object_id,
40
+ principalType: "App",
41
+ profile: {id: profile_id},
42
+ }.to_json
43
+ end
44
+ self.reload
45
+ end
46
+
47
+ def get_data
48
+ @tenant.get("/gateways/#{@gateway_datasource.gateway.id}/datasources/#{@gateway_datasource.id}/users", use_profile: false)[:value]
49
+ end
50
+ end
51
+ end
@@ -6,7 +6,7 @@ module PowerBI
6
6
  end
7
7
 
8
8
  def get_data(id)
9
- @tenant.get("/profiles/#{id}")
9
+ @tenant.get("/profiles/#{id}", use_profile: false)
10
10
  end
11
11
 
12
12
  def data_to_attributes(data)
@@ -17,7 +17,7 @@ module PowerBI
17
17
  end
18
18
 
19
19
  def delete
20
- @tenant.delete("/profiles/#{@id}")
20
+ @tenant.delete("/profiles/#{@id}", use_profile: false)
21
21
  @tenant.profiles.reload
22
22
  true
23
23
  end
@@ -30,7 +30,7 @@ module PowerBI
30
30
  end
31
31
 
32
32
  def create(name)
33
- data = @tenant.post("/profiles") do |req|
33
+ data = @tenant.post("/profiles", use_profile: false) do |req|
34
34
  req.body = {displayName: name}.to_json
35
35
  end
36
36
  self.reload
@@ -38,7 +38,7 @@ module PowerBI
38
38
  end
39
39
 
40
40
  def get_data
41
- @tenant.get("/profiles")[:value]
41
+ @tenant.get("/profiles", use_profile: false)[:value]
42
42
  end
43
43
  end
44
44
  end
@@ -22,18 +22,27 @@ module PowerBI
22
22
  end
23
23
 
24
24
  class RefreshArray < Array
25
+ attr_reader :entries_to_load
25
26
 
26
27
  def initialize(tenant, dataset)
27
28
  super(tenant, dataset)
28
29
  @dataset = dataset
30
+ @entries_to_load = 1
29
31
  end
30
32
 
31
33
  def self.get_class
32
34
  Refresh
33
35
  end
34
36
 
37
+ def entries_to_load=(entries_to_load)
38
+ if entries_to_load > @entries_to_load
39
+ @entries_to_load = entries_to_load
40
+ reload
41
+ end
42
+ end
43
+
35
44
  def get_data
36
- @tenant.get("/groups/#{@dataset.workspace.id}/datasets/#{@dataset.id}/refreshes", {'$top': '1'})[:value]
45
+ @tenant.get("/groups/#{@dataset.workspace.id}/datasets/#{@dataset.id}/refreshes", {'$top': @entries_to_load.to_s})[:value]
37
46
  end
38
47
  end
39
48
  end
@@ -1,6 +1,6 @@
1
1
  module PowerBI
2
2
  class Tenant
3
- attr_reader :workspaces, :gateways, :capacities, :profiles, :profile
3
+ attr_reader :workspaces, :gateways, :capacities, :profiles, :profile_id, :admin
4
4
 
5
5
  def initialize(token_generator, retries: 5, logger: nil)
6
6
  @token_generator = token_generator
@@ -9,7 +9,8 @@ module PowerBI
9
9
  @capacities = CapacityArray.new(self)
10
10
  @profiles = ProfileArray.new(self)
11
11
  @logger = logger
12
- @profile = nil
12
+ @profile_id = nil
13
+ @admin = Admin.new(self)
13
14
 
14
15
  ## WHY RETRIES? ##
15
16
  # It is noticed that once in a while (~0.1% API calls), the Power BI server returns a 500 (internal server error) without apparent reason, just retrying works :-)
@@ -44,12 +45,16 @@ module PowerBI
44
45
  Capacity.new(self, nil, id)
45
46
  end
46
47
 
48
+ def profile(id)
49
+ Profile.new(self, nil, id)
50
+ end
51
+
47
52
  def profile=(profile)
48
- @profile = profile
53
+ @profile_id = profile.is_a?(String) ? profile : profile&.id
49
54
  @workspaces.reload # we need to reload the workspaces because we look through the eyes of the profile
50
55
  end
51
56
 
52
- def get(url, params = {})
57
+ def get(url, params = {}, use_profile: true)
53
58
  t0 = Time.now
54
59
  conn = Faraday.new do |f|
55
60
  f.request :retry, @retry_options
@@ -58,8 +63,8 @@ module PowerBI
58
63
  req.params = params
59
64
  req.headers['Accept'] = 'application/json'
60
65
  req.headers['authorization'] = "Bearer #{token}"
61
- if @profile
62
- req.headers['X-PowerBI-Profile-Id'] = @profile.id
66
+ if use_profile
67
+ add_spp_header(req)
63
68
  end
64
69
  yield req if block_given?
65
70
  end
@@ -75,7 +80,7 @@ module PowerBI
75
80
  end
76
81
  end
77
82
 
78
- def get_raw(url, params = {})
83
+ def get_raw(url, params = {}, use_profile: true)
79
84
  t0 = Time.now
80
85
  conn = Faraday.new do |f|
81
86
  f.request :retry, @retry_options
@@ -83,8 +88,8 @@ module PowerBI
83
88
  response = conn.get(PowerBI::BASE_URL + url) do |req|
84
89
  req.params = params
85
90
  req.headers['authorization'] = "Bearer #{token}"
86
- if @profile
87
- req.headers['X-PowerBI-Profile-Id'] = @profile.id
91
+ if use_profile
92
+ add_spp_header(req)
88
93
  end
89
94
  yield req if block_given?
90
95
  end
@@ -95,7 +100,7 @@ module PowerBI
95
100
  response.body
96
101
  end
97
102
 
98
- def post(url, params = {})
103
+ def post(url, params = {}, use_profile: true)
99
104
  t0 = Time.now
100
105
  conn = Faraday.new do |f|
101
106
  f.request :retry, @retry_options
@@ -105,8 +110,8 @@ module PowerBI
105
110
  req.headers['Accept'] = 'application/json'
106
111
  req.headers['Content-Type'] = 'application/json'
107
112
  req.headers['authorization'] = "Bearer #{token}"
108
- if @profile
109
- req.headers['X-PowerBI-Profile-Id'] = @profile.id
113
+ if use_profile
114
+ add_spp_header(req)
110
115
  end
111
116
  yield req if block_given?
112
117
  end
@@ -119,7 +124,7 @@ module PowerBI
119
124
  end
120
125
  end
121
126
 
122
- def patch(url, params = {})
127
+ def patch(url, params = {}, use_profile: true)
123
128
  t0 = Time.now
124
129
  conn = Faraday.new do |f|
125
130
  f.request :retry, @retry_options
@@ -129,8 +134,8 @@ module PowerBI
129
134
  req.headers['Accept'] = 'application/json'
130
135
  req.headers['Content-Type'] = 'application/json'
131
136
  req.headers['authorization'] = "Bearer #{token}"
132
- if @profile
133
- req.headers['X-PowerBI-Profile-Id'] = @profile.id
137
+ if use_profile
138
+ add_spp_header(req)
134
139
  end
135
140
  yield req if block_given?
136
141
  end
@@ -143,7 +148,7 @@ module PowerBI
143
148
  end
144
149
  end
145
150
 
146
- def delete(url, params = {})
151
+ def delete(url, params = {}, use_profile: true)
147
152
  t0 = Time.now
148
153
  conn = Faraday.new do |f|
149
154
  f.request :retry, @retry_options
@@ -152,8 +157,8 @@ module PowerBI
152
157
  req.params = params
153
158
  req.headers['Accept'] = 'application/json'
154
159
  req.headers['authorization'] = "Bearer #{token}"
155
- if @profile
156
- req.headers['X-PowerBI-Profile-Id'] = @profile.id
160
+ if use_profile
161
+ add_spp_header(req)
157
162
  end
158
163
  yield req if block_given?
159
164
  end
@@ -169,7 +174,7 @@ module PowerBI
169
174
  end
170
175
  end
171
176
 
172
- def post_file(url, file, params = {})
177
+ def post_file(url, file, params = {}, use_profile: true)
173
178
  t0 = Time.now
174
179
  conn = Faraday.new do |f|
175
180
  f.request :multipart
@@ -180,8 +185,8 @@ module PowerBI
180
185
  req.headers['Accept'] = 'application/json'
181
186
  req.headers['Content-Type'] = 'multipart/form-data'
182
187
  req.headers['authorization'] = "Bearer #{token}"
183
- if @profile
184
- req.headers['X-PowerBI-Profile-Id'] = @profile.id
188
+ if use_profile
189
+ add_spp_header(req)
185
190
  end
186
191
  req.body = {value: Faraday::UploadIO.new(file, 'application/octet-stream')}
187
192
  req.options.timeout = 120 # default is 60 seconds Net::ReadTimeout
@@ -195,6 +200,12 @@ module PowerBI
195
200
 
196
201
  private
197
202
 
203
+ def add_spp_header(req)
204
+ if @profile_id
205
+ req.headers['X-PowerBI-Profile-Id'] = @profile_id
206
+ end
207
+ end
208
+
198
209
  def token
199
210
  @token_generator.call
200
211
  end
data/lib/power-bi.rb CHANGED
@@ -22,7 +22,9 @@ require_relative "power-bi/parameter"
22
22
  require_relative "power-bi/refresh"
23
23
  require_relative "power-bi/gateway"
24
24
  require_relative "power-bi/gateway_datasource"
25
+ require_relative "power-bi/gateway_datasource_user"
25
26
  require_relative "power-bi/page"
26
27
  require_relative "power-bi/user"
27
28
  require_relative "power-bi/capacity"
28
- require_relative "power-bi/profile"
29
+ require_relative "power-bi/profile"
30
+ require_relative "power-bi/admin"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: power-bi
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lode Cools
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-04 00:00:00.000000000 Z
11
+ date: 2024-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -89,12 +89,14 @@ files:
89
89
  - LICENSE
90
90
  - README.md
91
91
  - lib/power-bi.rb
92
+ - lib/power-bi/admin.rb
92
93
  - lib/power-bi/array.rb
93
94
  - lib/power-bi/capacity.rb
94
95
  - lib/power-bi/dataset.rb
95
96
  - lib/power-bi/datasource.rb
96
97
  - lib/power-bi/gateway.rb
97
98
  - lib/power-bi/gateway_datasource.rb
99
+ - lib/power-bi/gateway_datasource_user.rb
98
100
  - lib/power-bi/object.rb
99
101
  - lib/power-bi/page.rb
100
102
  - lib/power-bi/parameter.rb