power-bi 0.5.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +85 -0
- data/lib/power-bi.rb +2 -0
- data/lib/power-bi/dataset.rb +10 -0
- data/lib/power-bi/gateway.rb +25 -0
- data/lib/power-bi/gateway_datasource.rb +79 -0
- data/lib/power-bi/refresh.rb +1 -1
- data/lib/power-bi/report.rb +33 -0
- data/lib/power-bi/tenant.rb +64 -6
- data/lib/power-bi/workspace.rb +10 -4
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38722ac553ebd26a3d354aae65d622752817faa16c9a30af60a01bf719bcbba0
|
4
|
+
data.tar.gz: 2cffe049c3488a0e2f642293d48a9a0a25e81a39b619da32a0609ec1dc9ad029
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2e719c386fcc7b6e97878a5d01ace8795ae67f16b1e1c8f9848c012d3493c9c02ba7293e7f6b4d70b48d06ce75204c0455bc2a3c1f9610c05dbfc91bd17b48d
|
7
|
+
data.tar.gz: 4a27316227878ce178bb30d3d79f81863b0cb60a0923ee1c5bf24278691ccb6dff36efc19c06067090b23f203ff058c674e7aaceaa1acd2da00a607333932914
|
data/README.md
CHANGED
@@ -1,3 +1,88 @@
|
|
1
1
|
# power-bi
|
2
2
|
|
3
3
|
Ruby wrapper around the Power BI API
|
4
|
+
|
5
|
+
# Initialization
|
6
|
+
|
7
|
+
The Power BI API does not handle the authorization part. It requires the user to pass a function where it can request tokens.
|
8
|
+
|
9
|
+
```
|
10
|
+
pbi = PowerBI::Tenant.new(->{token = get_token(client, token) ; token.token})
|
11
|
+
```
|
12
|
+
|
13
|
+
# Supported endpoints
|
14
|
+
|
15
|
+
## Workspaces (aka Groups)
|
16
|
+
|
17
|
+
* List workspaces: `pbi.workspaces`
|
18
|
+
* Create workspace: `pbi.workspaces.create`
|
19
|
+
* Upload PBIX to workspace: `ws.upload_pbix('./test.pbix', 'new_datasetname_in_the_service')`
|
20
|
+
* Delete workspace: `workspace.delete`
|
21
|
+
* Add a user to a wokspace: `workspace.add_user('company_0001@fabrikam.com')`
|
22
|
+
|
23
|
+
## Reports
|
24
|
+
|
25
|
+
* List reports in a workspace: `workspace.reports`
|
26
|
+
* Clone a report from one workspace to another: `report.clone(src_workspace, new_report_name)`
|
27
|
+
* Rebind report to another dataset: `report.rebind(dataset)`
|
28
|
+
* Export report to file: `report.export_to_file(filenam, format: 'PDF')`
|
29
|
+
|
30
|
+
## Datasets
|
31
|
+
|
32
|
+
* List datasets in a workspace: `workspace.datasets`
|
33
|
+
* Update parameter in a dataset: `dataset.update_parameter(parameter_name, new_value)`
|
34
|
+
* Get time of last refresh: `dataset.last_refresh`
|
35
|
+
* Refresh the dataset: `dataset.refresh`
|
36
|
+
* Delete the dataset: `dataset.delete`
|
37
|
+
* Bind dataset to a gateway datasource: `dataset.bind_to_gateway(gateway, gateway_datasource)`
|
38
|
+
|
39
|
+
## Gateways
|
40
|
+
|
41
|
+
* List gateways: `pbi.gateways`
|
42
|
+
|
43
|
+
## Gateway datasources
|
44
|
+
|
45
|
+
* List datasources in a gateway: `gateway.gateway_datasources`
|
46
|
+
* Update credentials of a gateway datasource: `gateway_datasource.update_credentials(new_credentials)`
|
47
|
+
* Create a new gateway datasource: `gateway.gateway_datasource.create(name, credentials, db_server, db_name)`
|
48
|
+
* Delete a new gateway datasource: `gateway_datasource.delete`
|
49
|
+
|
50
|
+
# Note about gateway credentials
|
51
|
+
|
52
|
+
Power BI uses an obscure mechanism to encrypt credential exchange between the service and the gateway. The encryption must be done outside this module on a Windows machine based on th public key of the gateway. This is an example C# script:
|
53
|
+
|
54
|
+
```
|
55
|
+
using System;
|
56
|
+
using Microsoft.PowerBI.Api.Models;
|
57
|
+
using Microsoft.PowerBI.Api.Models.Credentials;
|
58
|
+
using Microsoft.PowerBI.Api.Extensions;
|
59
|
+
|
60
|
+
|
61
|
+
namespace pbi_credentials
|
62
|
+
{
|
63
|
+
class Program
|
64
|
+
{
|
65
|
+
static void Main(string[] args)
|
66
|
+
{
|
67
|
+
Console.WriteLine("Kicking off");
|
68
|
+
|
69
|
+
var credentials = new BasicCredentials(username: "cdmuser", password: "cdmuserpw4879515365");
|
70
|
+
|
71
|
+
var publicKey = new GatewayPublicKey("AQAB", "ru5gTdHbJ+8eC/uwERTOMz9Yktf/kCDWeRDCY1M5fPCB9+p4c8Uk54/NzT5ZWPQCp958bLcO8nSOSOpz4I8fW/AI4d+JxwW6VCsxzue2mKbJjeuSDXXmIiNUFqvjOIolfSIxJFNlfWkZUFlaD3dXgJkjJxrrc4OrYBDUt0FF14UsvdZymTbOl39sAhD4i9CqkXTqm6+JDxsEkPE3GAZ6ZslCsRUqu7lX73anAHkm889FR9NOMtsLV02JDMKCblJqnoszTzgExEEeoTJKxLiJdC8Mfbl96fKFS8JElJIzfTPzldGx5TxdjRmekQODWr7SNMSVJJQTJaANh9C2FZ85pQ==");
|
72
|
+
var credentialsEncryptor = new AsymmetricKeyEncryptor(publicKey);
|
73
|
+
|
74
|
+
var credentialDetails = new CredentialDetails(
|
75
|
+
credentials,
|
76
|
+
PrivacyLevel.Private,
|
77
|
+
EncryptedConnection.Encrypted,
|
78
|
+
credentialsEncryptor
|
79
|
+
);
|
80
|
+
Console.WriteLine(credentialDetails.Credentials);
|
81
|
+
|
82
|
+
Console.WriteLine("Bye Bye");
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
```
|
87
|
+
|
88
|
+
|
data/lib/power-bi.rb
CHANGED
data/lib/power-bi/dataset.rb
CHANGED
@@ -50,6 +50,16 @@ module PowerBI
|
|
50
50
|
true
|
51
51
|
end
|
52
52
|
|
53
|
+
def bind_to_gateway(gateway, gateway_datasource)
|
54
|
+
@tenant.post("/groups/#{workspace.id}/datasets/#{id}/Default.BindToGateway") do |req|
|
55
|
+
req.body = {
|
56
|
+
gatewayObjectId: gateway.id,
|
57
|
+
datasourceObjectIds: [gateway_datasource.id]
|
58
|
+
}.to_json
|
59
|
+
end
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
53
63
|
end
|
54
64
|
|
55
65
|
class DatasetArray < Array
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module PowerBI
|
2
|
+
class Gateway
|
3
|
+
attr_reader :name, :id, :type, :public_key, :gateway_datasources
|
4
|
+
|
5
|
+
def initialize(tenant, data)
|
6
|
+
@id = data[:id]
|
7
|
+
@name = data[:name]
|
8
|
+
@type = data[:type]
|
9
|
+
@public_key = data[:publicKey]
|
10
|
+
@tenant = tenant
|
11
|
+
@gateway_datasources = GatewayDatasourceArray.new(@tenant, self)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
class GatewayArray < Array
|
17
|
+
def self.get_class
|
18
|
+
Gateway
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_data
|
22
|
+
@tenant.get("/gateways")[:value]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module PowerBI
|
2
|
+
class GatewayDatasource
|
3
|
+
attr_reader :id, :gateway_id, :datasource_type, :connection_details, :credential_type, :datasource_name, :gateway
|
4
|
+
|
5
|
+
def initialize(tenant, data)
|
6
|
+
@gateway_id = data[:gatewayId]
|
7
|
+
@datasource_type = data[:datasourceType]
|
8
|
+
@datasource_name = data[:datasourceName]
|
9
|
+
@connection_details = data[:connectionDetails]
|
10
|
+
@id = data[:id]
|
11
|
+
@credential_type = data[:credentialType]
|
12
|
+
@gateway = data[:gateway]
|
13
|
+
@tenant = tenant
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_credentials(encrypted_credentials)
|
17
|
+
response = @tenant.patch("/gateways/#{gateway.id}/datasources/#{id}") do |req|
|
18
|
+
req.body = {
|
19
|
+
credentialDetails: {
|
20
|
+
credentialType: "Basic",
|
21
|
+
credentials: encrypted_credentials,
|
22
|
+
encryptedConnection: "Encrypted",
|
23
|
+
encryptionAlgorithm: "RSA-OAEP",
|
24
|
+
privacyLevel: "Organizational",
|
25
|
+
useCallerAADIdentity: false,
|
26
|
+
useEndUserOAuth2Credentials: false,
|
27
|
+
},
|
28
|
+
}.to_json
|
29
|
+
end
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete
|
34
|
+
@tenant.delete("/gateways/#{gateway.id}/datasources/#{id}")
|
35
|
+
@tenant.workspaces.reload
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class GatewayDatasourceArray < Array
|
42
|
+
|
43
|
+
def initialize(tenant, gateway)
|
44
|
+
super(tenant)
|
45
|
+
@gateway = gateway
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.get_class
|
49
|
+
GatewayDatasource
|
50
|
+
end
|
51
|
+
|
52
|
+
# only MySQL type is currently supported
|
53
|
+
def create(name, encrypted_credentials, db_server, db_name)
|
54
|
+
data = @tenant.post("/gateways/#{@gateway.id}/datasources",) do |req|
|
55
|
+
req.body = {
|
56
|
+
connectionDetails: {server: db_server, database: db_name}.to_json,
|
57
|
+
credentialDetails: {
|
58
|
+
credentialType: "Basic",
|
59
|
+
credentials: encrypted_credentials,
|
60
|
+
encryptedConnection: "Encrypted",
|
61
|
+
encryptionAlgorithm: "RSA-OAEP",
|
62
|
+
privacyLevel: "Organizational",
|
63
|
+
useCallerAADIdentity: false,
|
64
|
+
useEndUserOAuth2Credentials: false,
|
65
|
+
},
|
66
|
+
datasourceName: name,
|
67
|
+
datasourceType: 'MySql',
|
68
|
+
}.to_json
|
69
|
+
end
|
70
|
+
self.reload
|
71
|
+
GatewayDatasource.new(@tenant, data)
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_data
|
75
|
+
data = @tenant.get("/gateways/#{@gateway.id}/datasources")[:value]
|
76
|
+
data.each { |d| d[:gateway] = @gateway }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/power-bi/refresh.rb
CHANGED
@@ -6,7 +6,7 @@ module PowerBI
|
|
6
6
|
@id = data[:id]
|
7
7
|
@refresh_type = data[:refreshType]
|
8
8
|
@start_time = DateTime.iso8601(data[:startTime])
|
9
|
-
@end_time = DateTime.iso8601(data[:endTime])
|
9
|
+
@end_time = DateTime.iso8601(data[:endTime]) if data[:endTime]
|
10
10
|
@service_exception_json = data[:serviceExceptionJson]
|
11
11
|
@status = data[:status]
|
12
12
|
@request_id = data[:requestId]
|
data/lib/power-bi/report.rb
CHANGED
@@ -2,6 +2,8 @@ module PowerBI
|
|
2
2
|
class Report
|
3
3
|
attr_reader :name, :id, :report_type, :web_url, :embed_url, :is_from_pbix, :is_owned_by_me, :dataset_id, :workspace
|
4
4
|
|
5
|
+
class ExportToFileError < PowerBI::Error ; end
|
6
|
+
|
5
7
|
def initialize(tenant, data)
|
6
8
|
@id = data[:id]
|
7
9
|
@report_type = data[:reportType]
|
@@ -36,6 +38,37 @@ module PowerBI
|
|
36
38
|
true
|
37
39
|
end
|
38
40
|
|
41
|
+
def export_to_file(filename, format: 'PDF', timeout: 300)
|
42
|
+
# post
|
43
|
+
data = @tenant.post("/groups/#{workspace.id}/reports/#{id}/ExportTo") do |req|
|
44
|
+
req.body = {
|
45
|
+
format: format
|
46
|
+
}.to_json
|
47
|
+
end
|
48
|
+
export_id = data[:id]
|
49
|
+
|
50
|
+
# poll
|
51
|
+
success = false
|
52
|
+
iterations = 0
|
53
|
+
status_history = ''
|
54
|
+
old_status = ''
|
55
|
+
while !success
|
56
|
+
sleep 0.1
|
57
|
+
iterations += 1
|
58
|
+
raise ExportToFileError.new("Report export to file did not succeed after #{timeout} seconds. Status history:#{status_history}") if iterations > (10 * timeout)
|
59
|
+
new_status = @tenant.get("/groups/#{workspace.id}/reports/#{id}/exports/#{export_id}")[:status].to_s
|
60
|
+
success = (new_status == "Succeeded")
|
61
|
+
if new_status != old_status
|
62
|
+
status_history += "\nStatus change after #{iterations/10.0}s: '#{old_status}' --> '#{new_status}'"
|
63
|
+
old_status = new_status
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# get and write file
|
68
|
+
data = @tenant.get_raw("/groups/#{workspace.id}/reports/#{id}/exports/#{export_id}/file")
|
69
|
+
File.open(filename, "wb") { |f| f.write(data) }
|
70
|
+
end
|
71
|
+
|
39
72
|
end
|
40
73
|
|
41
74
|
class ReportArray < Array
|
data/lib/power-bi/tenant.rb
CHANGED
@@ -1,20 +1,37 @@
|
|
1
1
|
module PowerBI
|
2
2
|
class Tenant
|
3
|
-
attr_reader :workspaces
|
3
|
+
attr_reader :workspaces, :gateways
|
4
4
|
|
5
|
-
def initialize(token_generator)
|
5
|
+
def initialize(token_generator, retries: 5)
|
6
6
|
@token_generator = token_generator
|
7
7
|
@workspaces = WorkspaceArray.new(self)
|
8
|
+
@gateways = GatewayArray.new(self)
|
9
|
+
|
10
|
+
## WHY RETRIES? ##
|
11
|
+
# It is noticed that once in a while (~0.1% API calls), the Power BI server returns a 500 (internal server error) withou apparent reason, just retrying works :-)
|
12
|
+
##################
|
13
|
+
@retry_options = {
|
14
|
+
max: retries,
|
15
|
+
exceptions: [Errno::ETIMEDOUT, Timeout::Error, Faraday::TimeoutError, Faraday::RetriableResponse],
|
16
|
+
retry_statuses: [500], # internal server error
|
17
|
+
interval: 0.2,
|
18
|
+
interval_randomness: 0,
|
19
|
+
backoff_factor: 4,
|
20
|
+
retry_block: -> (env, options, retries, exc) { puts "retrying...!!" },
|
21
|
+
}
|
8
22
|
end
|
9
23
|
|
10
24
|
def get(url, params = {})
|
11
|
-
|
25
|
+
conn = Faraday.new do |f|
|
26
|
+
f.request :retry, @retry_options
|
27
|
+
end
|
28
|
+
response = conn.get(PowerBI::BASE_URL + url) do |req|
|
12
29
|
req.params = params
|
13
30
|
req.headers['Accept'] = 'application/json'
|
14
31
|
req.headers['authorization'] = "Bearer #{token}"
|
15
32
|
yield req if block_given?
|
16
33
|
end
|
17
|
-
|
34
|
+
unless [200, 202].include? response.status
|
18
35
|
raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
|
19
36
|
end
|
20
37
|
unless response.body.empty?
|
@@ -22,8 +39,45 @@ module PowerBI
|
|
22
39
|
end
|
23
40
|
end
|
24
41
|
|
42
|
+
def get_raw(url, params = {})
|
43
|
+
conn = Faraday.new do |f|
|
44
|
+
f.request :retry, @retry_options
|
45
|
+
end
|
46
|
+
response = conn.get(PowerBI::BASE_URL + url) do |req|
|
47
|
+
req.params = params
|
48
|
+
req.headers['authorization'] = "Bearer #{token}"
|
49
|
+
yield req if block_given?
|
50
|
+
end
|
51
|
+
unless [200, 202].include? response.status
|
52
|
+
raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
|
53
|
+
end
|
54
|
+
response.body
|
55
|
+
end
|
56
|
+
|
25
57
|
def post(url, params = {})
|
26
|
-
|
58
|
+
conn = Faraday.new do |f|
|
59
|
+
f.request :retry, @retry_options
|
60
|
+
end
|
61
|
+
response = conn.post(PowerBI::BASE_URL + url) do |req|
|
62
|
+
req.params = params
|
63
|
+
req.headers['Accept'] = 'application/json'
|
64
|
+
req.headers['Content-Type'] = 'application/json'
|
65
|
+
req.headers['authorization'] = "Bearer #{token}"
|
66
|
+
yield req if block_given?
|
67
|
+
end
|
68
|
+
unless [200, 201, 202].include? response.status
|
69
|
+
raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
|
70
|
+
end
|
71
|
+
unless response.body.empty?
|
72
|
+
JSON.parse(response.body, symbolize_names: true)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def patch(url, params = {})
|
77
|
+
conn = Faraday.new do |f|
|
78
|
+
f.request :retry, @retry_options
|
79
|
+
end
|
80
|
+
response = conn.patch(PowerBI::BASE_URL + url) do |req|
|
27
81
|
req.params = params
|
28
82
|
req.headers['Accept'] = 'application/json'
|
29
83
|
req.headers['Content-Type'] = 'application/json'
|
@@ -39,7 +93,10 @@ module PowerBI
|
|
39
93
|
end
|
40
94
|
|
41
95
|
def delete(url, params = {})
|
42
|
-
|
96
|
+
conn = Faraday.new do |f|
|
97
|
+
f.request :retry, @retry_options
|
98
|
+
end
|
99
|
+
response = conn.delete(PowerBI::BASE_URL + url) do |req|
|
43
100
|
req.params = params
|
44
101
|
req.headers['Accept'] = 'application/json'
|
45
102
|
req.headers['authorization'] = "Bearer #{token}"
|
@@ -56,6 +113,7 @@ module PowerBI
|
|
56
113
|
def post_file(url, file, params = {})
|
57
114
|
conn = Faraday.new do |f|
|
58
115
|
f.request :multipart
|
116
|
+
f.request :retry, @retry_options
|
59
117
|
end
|
60
118
|
response = conn.post(PowerBI::BASE_URL + url) do |req|
|
61
119
|
req.params = params
|
data/lib/power-bi/workspace.rb
CHANGED
@@ -14,17 +14,23 @@ module PowerBI
|
|
14
14
|
@datasets = DatasetArray.new(@tenant, self)
|
15
15
|
end
|
16
16
|
|
17
|
-
def upload_pbix(file, dataset_name)
|
17
|
+
def upload_pbix(file, dataset_name, timeout: 30)
|
18
18
|
data = @tenant.post_file("/groups/#{@id}/imports", file, {datasetDisplayName: dataset_name})
|
19
19
|
import_id = data[:id]
|
20
20
|
success = false
|
21
21
|
iterations = 0
|
22
|
+
status_history = ''
|
23
|
+
old_status = ''
|
22
24
|
while !success
|
23
25
|
sleep 0.1
|
24
26
|
iterations += 1
|
25
|
-
raise UploadError if iterations >
|
26
|
-
|
27
|
-
success = (
|
27
|
+
raise UploadError.new("Upload did not succeed after #{timeout} seconds. Status history:#{status_history}") if iterations > (10 * timeout)
|
28
|
+
new_status = @tenant.get("/groups/#{@id}/imports/#{import_id}")[:importState].to_s
|
29
|
+
success = (new_status == "Succeeded")
|
30
|
+
if new_status != old_status
|
31
|
+
status_history += "\nStatus change after #{iterations/10.0}s: '#{old_status}' --> '#{new_status}'"
|
32
|
+
old_status = new_status
|
33
|
+
end
|
28
34
|
end
|
29
35
|
@reports.reload
|
30
36
|
@datasets.reload
|
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:
|
4
|
+
version: 1.2.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: 2020-
|
11
|
+
date: 2020-11-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -38,7 +38,7 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.0'
|
41
|
-
description: Ruby wrapper for the Power BI API
|
41
|
+
description: Ruby wrapper for the Power BI API
|
42
42
|
email: lode.cools1@gmail.com
|
43
43
|
executables: []
|
44
44
|
extensions: []
|
@@ -50,6 +50,8 @@ files:
|
|
50
50
|
- lib/power-bi/array.rb
|
51
51
|
- lib/power-bi/dataset.rb
|
52
52
|
- lib/power-bi/datasource.rb
|
53
|
+
- lib/power-bi/gateway.rb
|
54
|
+
- lib/power-bi/gateway_datasource.rb
|
53
55
|
- lib/power-bi/parameter.rb
|
54
56
|
- lib/power-bi/refresh.rb
|
55
57
|
- lib/power-bi/report.rb
|
@@ -74,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
76
|
- !ruby/object:Gem::Version
|
75
77
|
version: '0'
|
76
78
|
requirements: []
|
77
|
-
rubygems_version: 3.
|
79
|
+
rubygems_version: 3.1.4
|
78
80
|
signing_key:
|
79
81
|
specification_version: 4
|
80
82
|
summary: Ruby wrapper for the Power BI API
|