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 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