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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a12ddf71e1ea1afad2cd5f4c7b059539cbc23da4a3e19834ad63bd6e62863ea
4
- data.tar.gz: 4960812cbfdcd0459afb47ff934177a289d5d0726e95e19c7d9f230b235fcbf6
3
+ metadata.gz: 1d25a34c3688310fb090dccae322ca49f7a48312246f3793b4e59cba25fd75a1
4
+ data.tar.gz: 5edb3f13333c5319f44fdac97769d3b5ee2adbb59f03296ceb664bc729017e77
5
5
  SHA512:
6
- metadata.gz: b3ca5f092ba2f179e77fdb16fa3f984886d59c4ba10b5d3744038d15c56f5fdf59e37962c9a1fb543b9babcf84eea425a916869431354a480eae99493526f1e1
7
- data.tar.gz: 02044abfa2f718990f7584405b8ea5a4d6a2314c2aa00ea6465f7ad2da24c863d8294601f526b9fee8914d5f5f5656355be4c168472dc05fa68ca3080427fa74
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
@@ -18,3 +18,5 @@ require_relative "power-bi/dataset"
18
18
  require_relative "power-bi/datasource"
19
19
  require_relative "power-bi/parameter"
20
20
  require_relative "power-bi/refresh"
21
+ require_relative "power-bi/gateway"
22
+ require_relative "power-bi/gateway_datasource"
@@ -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
@@ -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
- @tenant.post("/groups/#{workspace.id}/reports/#{id}/ExportTo") do |req|
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
@@ -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
- response = Faraday.get(PowerBI::BASE_URL + url) do |req|
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
- if response.status != 200
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
- response = Faraday.post(PowerBI::BASE_URL + url) do |req|
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
- response = Faraday.delete(PowerBI::BASE_URL + url) do |req|
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: 0.6.0
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 - Currently supports workspaces
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