power-bi 0.3.0 → 1.0.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: 61e60749c7e0da1010f9404770158dd257c2e4ea7bef82a6a97457dd85f8b7b8
4
- data.tar.gz: 017767f4e263817f1d0fec40f56869a5191bd1778d53ef383c2b7f97869b0068
3
+ metadata.gz: 418399b191274cab7954c37fa24d36f17cb2f802eff6654957e9b927fce9b9ee
4
+ data.tar.gz: 6f1681d24c0ad6b7a986753ccb53966a46a438b334cab39404e8584769daae0f
5
5
  SHA512:
6
- metadata.gz: b9e2c7483672138a160dfcca55ab7422c20ee3bb01499d959f2fde71cafff5b208acf8deb5dbbe3e80866f937b4a08bf99b653a9f35a120c3c56bad257c72e8a
7
- data.tar.gz: cccd0a870109d01e4db575487c402172144b9d0b928b9e62459c3a36cca5b4f09b3f5ac7274c187b7e9ea73b2aaad5bc6890d4411ad74aa61bb8124b5514e8f3
6
+ metadata.gz: 1bfedc133434941eeb869dc909e322b85d21059311d1089963c48a0acd4b86d6e1d2ef05b5f2e6cd646b3eea16a5937c4aef6c46aef063afe887fcb7c2c2cb4d
7
+ data.tar.gz: 136a28bcfb43052f5aca2717200a4abcb3a3f81b1bcca272ff5ac79b9dad5bedb9f395446a37f00c971ed89d5edeb72e694a4c3ae685040911ec6eb1280c3cf6
data/README.md CHANGED
@@ -1,3 +1,87 @@
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
+
49
+ # Note about gateway credentials
50
+
51
+ 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:
52
+
53
+ ```
54
+ using System;
55
+ using Microsoft.PowerBI.Api.Models;
56
+ using Microsoft.PowerBI.Api.Models.Credentials;
57
+ using Microsoft.PowerBI.Api.Extensions;
58
+
59
+
60
+ namespace pbi_credentials
61
+ {
62
+ class Program
63
+ {
64
+ static void Main(string[] args)
65
+ {
66
+ Console.WriteLine("Kicking off");
67
+
68
+ var credentials = new BasicCredentials(username: "cdmuser", password: "cdmuserpw4879515365");
69
+
70
+ var publicKey = new GatewayPublicKey("AQAB", "ru5gTdHbJ+8eC/uwERTOMz9Yktf/kCDWeRDCY1M5fPCB9+p4c8Uk54/NzT5ZWPQCp958bLcO8nSOSOpz4I8fW/AI4d+JxwW6VCsxzue2mKbJjeuSDXXmIiNUFqvjOIolfSIxJFNlfWkZUFlaD3dXgJkjJxrrc4OrYBDUt0FF14UsvdZymTbOl39sAhD4i9CqkXTqm6+JDxsEkPE3GAZ6ZslCsRUqu7lX73anAHkm889FR9NOMtsLV02JDMKCblJqnoszTzgExEEeoTJKxLiJdC8Mfbl96fKFS8JElJIzfTPzldGx5TxdjRmekQODWr7SNMSVJJQTJaANh9C2FZ85pQ==");
71
+ var credentialsEncryptor = new AsymmetricKeyEncryptor(publicKey);
72
+
73
+ var credentialDetails = new CredentialDetails(
74
+ credentials,
75
+ PrivacyLevel.Private,
76
+ EncryptedConnection.Encrypted,
77
+ credentialsEncryptor
78
+ );
79
+ Console.WriteLine(credentialDetails.Credentials);
80
+
81
+ Console.WriteLine("Bye Bye");
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+
data/lib/power-bi.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'pry' # TODO remove in final product
2
2
  require 'faraday'
3
3
  require 'json'
4
+ require 'date'
4
5
 
5
6
  module PowerBI
6
7
  BASE_URL = 'https://api.powerbi.com/v1.0/myorg'
@@ -16,3 +17,6 @@ require_relative "power-bi/report"
16
17
  require_relative "power-bi/dataset"
17
18
  require_relative "power-bi/datasource"
18
19
  require_relative "power-bi/parameter"
20
+ require_relative "power-bi/refresh"
21
+ require_relative "power-bi/gateway"
22
+ require_relative "power-bi/gateway_datasource"
@@ -2,7 +2,7 @@ module PowerBI
2
2
  class Dataset
3
3
  attr_reader :id, :name, :add_rows_API_enabled, :configured_by, :is_refreshable, :is_effective_identity_required,
4
4
  :is_effective_identity_roles_required, :is_on_prem_gateway_required, :target_storage_mode, :workspace, :datasources,
5
- :parameters
5
+ :parameters, :refresh_history
6
6
 
7
7
  def initialize(tenant, data)
8
8
  @id = data[:id]
@@ -18,6 +18,7 @@ module PowerBI
18
18
  @tenant = tenant
19
19
  @datasources = DatasourceArray.new(@tenant, self)
20
20
  @parameters = ParameterArray.new(@tenant, self)
21
+ @refresh_history = RefreshArray.new(@tenant, self)
21
22
  end
22
23
 
23
24
  def update_parameter(name, value)
@@ -30,6 +31,9 @@ module PowerBI
30
31
  true
31
32
  end
32
33
 
34
+ def last_refresh
35
+ @refresh_history.first
36
+ end
33
37
 
34
38
  def refresh
35
39
  @tenant.post("/groups/#{workspace.id}/datasets/#{id}/refreshes") do |req|
@@ -37,6 +41,7 @@ module PowerBI
37
41
  notifyOption: "NoNotification"
38
42
  }.to_json
39
43
  end
44
+ @refresh_history.reload
40
45
  true
41
46
  end
42
47
 
@@ -45,6 +50,16 @@ module PowerBI
45
50
  true
46
51
  end
47
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
+
48
63
  end
49
64
 
50
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,73 @@
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
+ end
34
+
35
+ class GatewayDatasourceArray < Array
36
+
37
+ def initialize(tenant, gateway)
38
+ super(tenant)
39
+ @gateway = gateway
40
+ end
41
+
42
+ def self.get_class
43
+ GatewayDatasource
44
+ end
45
+
46
+ # only MySQL type is currently supported
47
+ def create(name, encrypted_credentials, db_server, db_name)
48
+ data = @tenant.post("/gateways/#{@gateway.id}/datasources",) do |req|
49
+ req.body = {
50
+ connectionDetails: {server: db_server, database: db_name}.to_json,
51
+ credentialDetails: {
52
+ credentialType: "Basic",
53
+ credentials: encrypted_credentials,
54
+ encryptedConnection: "Encrypted",
55
+ encryptionAlgorithm: "RSA-OAEP",
56
+ privacyLevel: "Organizational",
57
+ useCallerAADIdentity: false,
58
+ useEndUserOAuth2Credentials: false,
59
+ },
60
+ datasourceName: name,
61
+ datasourceType: 'MySql',
62
+ }.to_json
63
+ end
64
+ self.reload
65
+ GatewayDatasource.new(@tenant, data)
66
+ end
67
+
68
+ def get_data
69
+ data = @tenant.get("/gateways/#{@gateway.id}/datasources")[:value]
70
+ data.each { |d| d[:gateway] = @gateway }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,32 @@
1
+ module PowerBI
2
+ class Refresh
3
+ attr_reader :refresh_type, :start_time, :end_time, :service_exception_json, :status, :request_id
4
+
5
+ def initialize(tenant, data)
6
+ @id = data[:id]
7
+ @refresh_type = data[:refreshType]
8
+ @start_time = DateTime.iso8601(data[:startTime])
9
+ @end_time = DateTime.iso8601(data[:endTime]) if data[:endTime]
10
+ @service_exception_json = data[:serviceExceptionJson]
11
+ @status = data[:status]
12
+ @request_id = data[:requestId]
13
+ end
14
+
15
+ end
16
+
17
+ class RefreshArray < Array
18
+
19
+ def initialize(tenant, dataset)
20
+ super(tenant)
21
+ @dataset = dataset
22
+ end
23
+
24
+ def self.get_class
25
+ Refresh
26
+ end
27
+
28
+ def get_data
29
+ @tenant.get("/groups/#{@dataset.workspace.id}/datasets/#{@dataset.id}/refreshes", {'$top': '3'})[:value]
30
+ end
31
+ end
32
+ 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,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,10 +1,11 @@
1
1
  module PowerBI
2
2
  class Tenant
3
- attr_reader :workspaces
3
+ attr_reader :workspaces, :gateways
4
4
 
5
5
  def initialize(token_generator)
6
6
  @token_generator = token_generator
7
7
  @workspaces = WorkspaceArray.new(self)
8
+ @gateways = GatewayArray.new(self)
8
9
  end
9
10
 
10
11
  def get(url, params = {})
@@ -14,7 +15,7 @@ module PowerBI
14
15
  req.headers['authorization'] = "Bearer #{token}"
15
16
  yield req if block_given?
16
17
  end
17
- if response.status != 200
18
+ unless [200, 202].include? response.status
18
19
  raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
19
20
  end
20
21
  unless response.body.empty?
@@ -22,6 +23,18 @@ module PowerBI
22
23
  end
23
24
  end
24
25
 
26
+ def get_raw(url, params = {})
27
+ response = Faraday.get(PowerBI::BASE_URL + url) do |req|
28
+ req.params = params
29
+ req.headers['authorization'] = "Bearer #{token}"
30
+ yield req if block_given?
31
+ end
32
+ unless [200, 202].include? response.status
33
+ raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
34
+ end
35
+ response.body
36
+ end
37
+
25
38
  def post(url, params = {})
26
39
  response = Faraday.post(PowerBI::BASE_URL + url) do |req|
27
40
  req.params = params
@@ -30,6 +43,22 @@ module PowerBI
30
43
  req.headers['authorization'] = "Bearer #{token}"
31
44
  yield req if block_given?
32
45
  end
46
+ unless [200, 201, 202].include? response.status
47
+ raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
48
+ end
49
+ unless response.body.empty?
50
+ JSON.parse(response.body, symbolize_names: true)
51
+ end
52
+ end
53
+
54
+ def patch(url, params = {})
55
+ response = Faraday.patch(PowerBI::BASE_URL + url) do |req|
56
+ req.params = params
57
+ req.headers['Accept'] = 'application/json'
58
+ req.headers['Content-Type'] = 'application/json'
59
+ req.headers['authorization'] = "Bearer #{token}"
60
+ yield req if block_given?
61
+ end
33
62
  unless [200, 202].include? response.status
34
63
  raise APIError.new("Error calling Power BI API (status #{response.status}): #{response.body}")
35
64
  end
@@ -14,23 +14,35 @@ 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
31
37
  true
32
38
  end
33
39
 
40
+ def delete
41
+ @tenant.delete("/groups/#{@id}")
42
+ @tenant.workspaces.reload
43
+ true
44
+ end
45
+
34
46
  # TODO LATER: the 'Viewer' acces right is currently not settable throught the API. Fix that later
35
47
  def add_user(email_address, access_right = 'Member')
36
48
  @tenant.post("/groups/#{id}/users") do |req|
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.3.0
4
+ version: 1.0.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,7 +50,10 @@ 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
56
+ - lib/power-bi/refresh.rb
54
57
  - lib/power-bi/report.rb
55
58
  - lib/power-bi/tenant.rb
56
59
  - lib/power-bi/workspace.rb
@@ -73,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
76
  - !ruby/object:Gem::Version
74
77
  version: '0'
75
78
  requirements: []
76
- rubygems_version: 3.0.3
79
+ rubygems_version: 3.1.4
77
80
  signing_key:
78
81
  specification_version: 4
79
82
  summary: Ruby wrapper for the Power BI API