power-bi 0.5.0 → 1.2.0

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: b5ef83e5bdcaaa737a8bd83a825b597d302d70b31ff35b3aff95a6b3c1c7f2a4
4
- data.tar.gz: '019b0bd626eccac2f699d500d3eec467cfa209ee1658cd85e5ee9323856e4fa8'
3
+ metadata.gz: 38722ac553ebd26a3d354aae65d622752817faa16c9a30af60a01bf719bcbba0
4
+ data.tar.gz: 2cffe049c3488a0e2f642293d48a9a0a25e81a39b619da32a0609ec1dc9ad029
5
5
  SHA512:
6
- metadata.gz: a0432e8d536199d1edacb320ddabf5a4abc819a1c2e565c647727c12cad819f10696043d57b102611e5adcf5facf8553f2d2702cd6879fd24a82f335033ba95b
7
- data.tar.gz: ed395225b4a0a26645a167cb17319ee5c8714195680fbbbb13e533f6293152de58aae3fc823b7540b0dcb26f1523e014be7e107f66386e61b7756508a51c8374
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
@@ -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"
@@ -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
@@ -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]
@@ -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
@@ -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
- response = Faraday.get(PowerBI::BASE_URL + url) do |req|
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
- if response.status != 200
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
- response = Faraday.post(PowerBI::BASE_URL + url) do |req|
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
- response = Faraday.delete(PowerBI::BASE_URL + url) do |req|
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
@@ -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 > 300 # 30 seconds
26
- status = @tenant.get("/groups/#{@id}/imports/#{import_id}")
27
- success = (status[:importState] == "Succeeded")
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: 0.5.0
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-01-27 00:00:00.000000000 Z
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 - 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
@@ -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.0.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