power-bi 0.6.0 → 1.3.1
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 +11 -0
- data/lib/power-bi/gateway.rb +25 -0
- data/lib/power-bi/gateway_datasource.rb +79 -0
- data/lib/power-bi/report.rb +27 -2
- data/lib/power-bi/tenant.rb +66 -6
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d25a34c3688310fb090dccae322ca49f7a48312246f3793b4e59cba25fd75a1
|
4
|
+
data.tar.gz: 5edb3f13333c5319f44fdac97769d3b5ee2adbb59f03296ceb664bc729017e77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db57e4e5fb379b3cab50becc7060d19caf48cbcd366aa2231829c988ff448af01cf40ab9f189c8607b26bfaa8c96a8efd14df3d3337d1bbc257fc41315386637
|
7
|
+
data.tar.gz: 390c4de1b57462be8817be336b35de6e24e488569cad47936d116efc922e382b804c71b16717bbe31fb38aa486e3a946ad69a9816055832da90507f2cf1a4678
|
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
@@ -47,6 +47,17 @@ module PowerBI
|
|
47
47
|
|
48
48
|
def delete
|
49
49
|
@tenant.delete("/groups/#{workspace.id}/datasets/#{id}")
|
50
|
+
@workspace.datasets.reload
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def bind_to_gateway(gateway, gateway_datasource)
|
55
|
+
@tenant.post("/groups/#{workspace.id}/datasets/#{id}/Default.BindToGateway") do |req|
|
56
|
+
req.body = {
|
57
|
+
gatewayObjectId: gateway.id,
|
58
|
+
datasourceObjectIds: [gateway_datasource.id]
|
59
|
+
}.to_json
|
60
|
+
end
|
50
61
|
true
|
51
62
|
end
|
52
63
|
|
@@ -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
|
+
@gateway.gateway_datasources.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/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,12 +38,35 @@ module PowerBI
|
|
36
38
|
true
|
37
39
|
end
|
38
40
|
|
39
|
-
def export_to_file(format: 'PDF')
|
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|
|
41
44
|
req.body = {
|
42
45
|
format: format
|
43
46
|
}.to_json
|
44
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) }
|
45
70
|
end
|
46
71
|
|
47
72
|
end
|
data/lib/power-bi/tenant.rb
CHANGED
@@ -1,20 +1,38 @@
|
|
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
|
+
methods: [:get, :post, :patch, :delete],
|
17
|
+
retry_statuses: [500], # internal server error
|
18
|
+
interval: 0.2,
|
19
|
+
interval_randomness: 0,
|
20
|
+
backoff_factor: 4,
|
21
|
+
retry_block: -> (env, options, retries, exc) { puts "retrying...!! (@ #{Time.now.to_s}), exception: #{exc.to_s} ---- #{exc.message}" },
|
22
|
+
}
|
8
23
|
end
|
9
24
|
|
10
25
|
def get(url, params = {})
|
11
|
-
|
26
|
+
conn = Faraday.new do |f|
|
27
|
+
f.request :retry, @retry_options
|
28
|
+
end
|
29
|
+
response = conn.get(PowerBI::BASE_URL + url) do |req|
|
12
30
|
req.params = params
|
13
31
|
req.headers['Accept'] = 'application/json'
|
14
32
|
req.headers['authorization'] = "Bearer #{token}"
|
15
33
|
yield req if block_given?
|
16
34
|
end
|
17
|
-
|
35
|
+
unless [200, 202].include? response.status
|
18
36
|
raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
|
19
37
|
end
|
20
38
|
unless response.body.empty?
|
@@ -22,8 +40,45 @@ module PowerBI
|
|
22
40
|
end
|
23
41
|
end
|
24
42
|
|
43
|
+
def get_raw(url, params = {})
|
44
|
+
conn = Faraday.new do |f|
|
45
|
+
f.request :retry, @retry_options
|
46
|
+
end
|
47
|
+
response = conn.get(PowerBI::BASE_URL + url) do |req|
|
48
|
+
req.params = params
|
49
|
+
req.headers['authorization'] = "Bearer #{token}"
|
50
|
+
yield req if block_given?
|
51
|
+
end
|
52
|
+
unless [200, 202].include? response.status
|
53
|
+
raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
|
54
|
+
end
|
55
|
+
response.body
|
56
|
+
end
|
57
|
+
|
25
58
|
def post(url, params = {})
|
26
|
-
|
59
|
+
conn = Faraday.new do |f|
|
60
|
+
f.request :retry, @retry_options
|
61
|
+
end
|
62
|
+
response = conn.post(PowerBI::BASE_URL + url) do |req|
|
63
|
+
req.params = params
|
64
|
+
req.headers['Accept'] = 'application/json'
|
65
|
+
req.headers['Content-Type'] = 'application/json'
|
66
|
+
req.headers['authorization'] = "Bearer #{token}"
|
67
|
+
yield req if block_given?
|
68
|
+
end
|
69
|
+
unless [200, 201, 202].include? response.status
|
70
|
+
raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
|
71
|
+
end
|
72
|
+
unless response.body.empty?
|
73
|
+
JSON.parse(response.body, symbolize_names: true)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def patch(url, params = {})
|
78
|
+
conn = Faraday.new do |f|
|
79
|
+
f.request :retry, @retry_options
|
80
|
+
end
|
81
|
+
response = conn.patch(PowerBI::BASE_URL + url) do |req|
|
27
82
|
req.params = params
|
28
83
|
req.headers['Accept'] = 'application/json'
|
29
84
|
req.headers['Content-Type'] = 'application/json'
|
@@ -39,7 +94,10 @@ module PowerBI
|
|
39
94
|
end
|
40
95
|
|
41
96
|
def delete(url, params = {})
|
42
|
-
|
97
|
+
conn = Faraday.new do |f|
|
98
|
+
f.request :retry, @retry_options
|
99
|
+
end
|
100
|
+
response = conn.delete(PowerBI::BASE_URL + url) do |req|
|
43
101
|
req.params = params
|
44
102
|
req.headers['Accept'] = 'application/json'
|
45
103
|
req.headers['authorization'] = "Bearer #{token}"
|
@@ -56,6 +114,7 @@ module PowerBI
|
|
56
114
|
def post_file(url, file, params = {})
|
57
115
|
conn = Faraday.new do |f|
|
58
116
|
f.request :multipart
|
117
|
+
f.request :retry, @retry_options
|
59
118
|
end
|
60
119
|
response = conn.post(PowerBI::BASE_URL + url) do |req|
|
61
120
|
req.params = params
|
@@ -63,6 +122,7 @@ module PowerBI
|
|
63
122
|
req.headers['Content-Type'] = 'multipart/form-data'
|
64
123
|
req.headers['authorization'] = "Bearer #{token}"
|
65
124
|
req.body = {value: Faraday::UploadIO.new(file, 'application/octet-stream')}
|
125
|
+
req.options.timeout = 120 # default is 60 seconds Net::ReadTimeout
|
66
126
|
end
|
67
127
|
if response.status != 202
|
68
128
|
raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: power-bi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lode Cools
|
@@ -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
|