power-bi 0.6.0 → 1.3.1
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 +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
|